Skip to content

M3/M4 full-issue closure wave: onboarding, ops hardening, release strategy#48

Merged
Prekzursil merged 3 commits intomainfrom
feature/full-issue-closure-m3-m4
Feb 18, 2026
Merged

M3/M4 full-issue closure wave: onboarding, ops hardening, release strategy#48
Prekzursil merged 3 commits intomainfrom
feature/full-issue-closure-m3-m4

Conversation

@Prekzursil
Copy link
Owner

@Prekzursil Prekzursil commented Feb 18, 2026

User description

Summary

Implements the post-cutoff full-issue closure wave for deterministic and ops/product surfaces:

  • M2 corpus coverage completion (SaveCorpusRoundTripTests + tracked fixtures)
  • M3 Mod Compatibility Studio contracts/services/UI/tests/docs
  • M4 transactional profile update + support bundle + telemetry services/UI/tests/docs
  • deployment strategy hardening for GitHub Releases (Pages/deployment strategy (deferred after CI/security baseline) #29) with checksums + release runbook
  • ops contract/schema hardening and CI schema-smoke expansion

Affected Profiles

  • base_sweaw
  • base_swfoc
  • aotr_1397421866_swfoc
  • roe_3447786229_swfoc
  • custom_* (M3 onboarding drafts)

Reliability Evidence

Repro bundle JSON: justified skip (deterministic/tooling/docs/release workflow changes; no live runtime behavior change claimed in this PR)
Classification: justified skip
Launch reason code(s): n/a (live evidence remains tracked in #19/#34)

Deterministic verification run in this branch:

  • dotnet build SwfocTrainer.sln -c Release --no-restore
  • dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName!~SwfocTrainer.Tests.Profiles.Live&FullyQualifiedName!~RuntimeAttachSmokeTests"
  • dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~LaunchContext"
  • dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ModDependencyValidator"
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/validate-policy-contracts.ps1
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/validate-repro-bundle.ps1 -BundlePath tools/fixtures/repro_bundle_sample.json -SchemaPath tools/schemas/repro-bundle.schema.json -Strict
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/validate-save-patch-pack.ps1 -PatchPackPath tools/fixtures/save_patch_pack_sample.json -SchemaPath tools/schemas/save-patch-pack.schema.json -Strict
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/validate-calibration-artifact.ps1 -ArtifactPath tools/fixtures/calibration_artifact_sample.json -SchemaPath tools/schemas/calibration-artifact.schema.json -Strict
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/validate-support-bundle-manifest.ps1 -ManifestPath tools/fixtures/support_bundle_manifest_sample.json -SchemaPath tools/schemas/support-bundle-manifest.schema.json -Strict
  • powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools/compare-visual-pack.ps1 -BaselineDir tools/fixtures/visual/baseline -CandidateDir tools/fixtures/visual/candidate -OutputPath TestResults/visual-compare-smoke.json
  • python3 tools/detect-launch-context.py --from-process-json tools/fixtures/launch_context_cases.json --profile-root profiles/default --pretty > TestResults/launch_context_results_phase2.json

Issue Mapping

Closes #8
Closes #9
Closes #10
Closes #29
Closes #39
Closes #40
Closes #41
Closes #42
Closes #43
Closes #44
Refs #6
Refs #7
Refs #19
Refs #34


PR Type

Enhancement, Tests, Documentation


Description

M3/M4 Full-Issue Closure Wave: Onboarding, Ops Hardening, and Release Strategy

Core Feature Implementations:

  • M3 Mod Compatibility Studio: Custom mod profile scaffolding from launch samples with Steam Workshop ID and local path inference; calibration artifact export with symbol candidate tracking; compatibility report generation with promotion readiness blocking

  • M4 Transactional Profile Updates: Comprehensive profile installation with backup creation, SHA256 verification, and rollback support via timestamped backups and install receipts

  • M4 Support Bundle & Telemetry: Zipped diagnostics export aggregating logs, calibration artifacts, repro bundles, runtime snapshots, and telemetry metrics; thread-safe action recording with failure/fallback/unresolved rate computation

  • M2 Save Corpus Coverage: Round-trip codec validation for all shipped save schemas (base SWEAW, base SWFOC, AOTR, ROE) using tracked deterministic fixtures

Ops & Release Hardening:

  • GitHub Releases Workflow: Portable zip artifact generation with SHA256 checksum verification; tag-driven and manual dispatch support; release notes templating

  • Schema & Contract Validation: New JSON schemas for calibration artifacts and support bundle manifests; policy contract enforcement; CI schema smoke tests

  • SonarCloud Conditional Execution: Graceful skip when token unavailable; visual audit environment variable configuration

UI & Service Integration:

  • MainViewModel Expansion: Four new service dependencies; nine new properties for onboarding state; five async command handlers for scaffolding, export, and reporting

  • MainWindow Layout Redesign: "Mod Compatibility Studio" group with base/draft profile fields; "Ops Hardening" group with bundle/telemetry exports; three-row Profiles & Updates tab

  • DI Container Registration: All M3/M4 service implementations registered in App.xaml.cs

Documentation & Tracking:

  • Release runbook with artifact verification and rollback procedures

  • Release notes template with highlights and known limitations

  • Profile format documentation for M3 onboarding and custom namespaces

  • Calibration evidence payload standardization

  • Save Lab runbook enhancements for cross-profile compatibility and fault tolerance

  • TODO.md completion tracking for M2/M3/M4 milestones

Bug Fixes:

  • Visual pack comparison path normalization (regex-based leading separator trimming)

Diagram Walkthrough

flowchart LR
  LaunchSamples["Launch Samples<br/>STEAMMOD/MODPATH hints"]
  ModOnboarding["ModOnboardingService<br/>ScaffoldDraftProfileAsync"]
  DraftProfile["Draft Profile<br/>custom_* namespace"]
  
  ProfileUpdate["GitHubProfileUpdateService<br/>InstallProfileTransactionalAsync"]
  Backup["Timestamped Backup<br/>+ Install Receipt"]
  Rollback["RollbackLastInstallAsync<br/>+ Rollback Receipt"]
  
  ModCalibration["ModCalibrationService<br/>ExportCalibrationArtifactAsync"]
  CompatReport["BuildCompatibilityReportAsync<br/>Promotion Readiness"]
  CalibArtifact["Calibration Artifact<br/>Symbol Candidates"]
  
  Telemetry["TelemetrySnapshotService<br/>RecordAction + CreateSnapshot"]
  Metrics["Telemetry Snapshot<br/>Failure/Fallback/Unresolved Rates"]
  
  SupportBundle["SupportBundleService<br/>ExportAsync"]
  Bundle["Support Bundle ZIP<br/>Logs + Artifacts + Telemetry"]
  
  GitHubRelease["release-portable.yml<br/>SHA256 Checksum"]
  Release["GitHub Release<br/>Portable ZIP + Checksum"]
  
  SaveCorpus["SaveCorpusRoundTripTests<br/>Deterministic Fixtures"]
  Coverage["M2 Corpus Coverage<br/>4 Schema Profiles"]
  
  LaunchSamples --> ModOnboarding --> DraftProfile
  DraftProfile --> ProfileUpdate --> Backup
  Backup --> Rollback
  DraftProfile --> ModCalibration --> CalibArtifact
  CalibArtifact --> CompatReport
  ProfileUpdate --> Telemetry --> Metrics
  Metrics --> SupportBundle --> Bundle
  Bundle --> GitHubRelease --> Release
  SaveCorpus --> Coverage
Loading

File Walkthrough

Relevant files
Enhancement
23 files
MainViewModel.cs
M3/M4 UI integration with onboarding and ops services       

src/SwfocTrainer.App/ViewModels/MainViewModel.cs

  • Added four new service dependencies (IModOnboardingService,
    IModCalibrationService, ISupportBundleService,
    ITelemetrySnapshotService) to support M3/M4 features
  • Added nine new view model properties for mod onboarding UI state (base
    profile, draft profile, display name, launch samples, etc.)
  • Added five new async command handlers for scaffolding profiles,
    exporting calibration artifacts, building compatibility reports, and
    exporting support/telemetry bundles
  • Added ModCompatibilityRows observable collection and updated
    InstallUpdateAsync to use transactional install result with
    receipt/backup tracking
+223/-2 
GitHubProfileUpdateService.cs
Transactional profile updates with rollback support           

src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs

  • Refactored InstallProfileAsync to wrap new
    InstallProfileTransactionalAsync method for backward compatibility
  • Implemented InstallProfileTransactionalAsync with comprehensive error
    handling, returning structured ProfileInstallResult with
    success/failure details, backup paths, and install receipts
  • Added RollbackLastInstallAsync method to restore profiles from
    timestamped backups with rollback receipt generation
  • Added helper methods WriteInstallReceiptAsync and
    WriteRollbackReceiptAsync to track installation/rollback operations
    with JSON metadata
+263/-13
ModOnboardingService.cs
Custom mod profile scaffolding from launch samples             

src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs

  • Implemented ScaffoldDraftProfileAsync to generate custom mod profiles
    from launch samples with base profile inheritance
  • Infers Steam Workshop IDs from STEAMMOD= markers and local path hints
    from MODPATH= tokens and process paths
  • Normalizes profile IDs and namespaces, generates profile aliases, and
    writes draft profiles to custom namespace directories
  • Validates input requirements and returns warnings for missing
    workshop/path hints
+299/-0 
SupportBundleService.cs
Support bundle export with diagnostics aggregation             

src/SwfocTrainer.Core/Services/SupportBundleService.cs

  • Implements ExportAsync to create zipped support bundles containing
    logs, calibration artifacts, recent repro bundles, runtime snapshots,
    and telemetry
  • Copies up to 10 recent log files and 8 calibration artifacts from
    LocalAppData directories
  • Generates runtime snapshot JSON with process metadata and symbol
    health summary when session is attached
  • Creates manifest.json with schema version, included files list, and
    warnings for missing directories
+251/-0 
ModCalibrationService.cs
Calibration artifact export and compatibility reporting   

src/SwfocTrainer.Core/Services/ModCalibrationService.cs

  • Implements ExportCalibrationArtifactAsync to write symbol candidate
    artifacts with module fingerprint, process metadata, and health status
  • Implements BuildCompatibilityReportAsync to compute promotion
    readiness by evaluating action reliability, dependency validation, and
    critical symbol resolution
  • Blocks promotion when dependency validation fails, critical symbols
    are unresolved, or actions are unavailable
  • Parses critical symbol metadata from profile and generates detailed
    compatibility notes
+228/-0 
TelemetrySnapshotService.cs
Telemetry snapshot service for runtime diagnostics             

src/SwfocTrainer.Core/Services/TelemetrySnapshotService.cs

  • Implements thread-safe action recording with success/failure counters
    and address source tracking
  • Provides CreateSnapshot to compute aggregated metrics (failure rate,
    fallback rate, unresolved rate)
  • Implements ExportSnapshotAsync to serialize telemetry to JSON with
    timestamp
  • Includes Reset method for clearing counters
+98/-0   
ModOnboardingModels.cs
Mod onboarding and calibration data models                             

src/SwfocTrainer.Core/Models/ModOnboardingModels.cs

  • Defines ModLaunchSample record for process/command-line capture
  • Defines ModOnboardingRequest and ModOnboardingResult contracts for
    profile scaffolding
  • Defines ModCalibrationArtifactRequest and ModCalibrationArtifactResult
    for artifact export
  • Defines CalibrationCandidate record for symbol entries in artifacts
+62/-0   
TrainerOrchestrator.cs
Telemetry integration into action orchestration                   

src/SwfocTrainer.Core/Services/TrainerOrchestrator.cs

  • Added optional ITelemetrySnapshotService dependency with default
    instantiation
  • Records action execution results (success/failure and address source)
    to telemetry service after each action
+5/-1     
CompatibilityReportModels.cs
Compatibility report data models                                                 

src/SwfocTrainer.Core/Models/CompatibilityReportModels.cs

  • Defines ModActionCompatibility record for per-action reliability state
  • Defines ModCompatibilityReport record with promotion readiness
    verdict, dependency status, and action compatibility list
+23/-0   
IModCalibrationService.cs
Mod calibration service contract                                                 

src/SwfocTrainer.Core/Contracts/IModCalibrationService.cs

  • Defines interface for calibration artifact export and compatibility
    report generation
  • Supports optional dependency validation and catalog parameters for
    advanced analysis
+18/-0   
ProfileUpdateModels.cs
Profile update operation result models                                     

src/SwfocTrainer.Core/Models/ProfileUpdateModels.cs

  • Defines ProfileInstallResult record with success flag, paths, message,
    and reason code
  • Defines ProfileRollbackResult record with restoration status and
    backup tracking
+24/-0   
SupportBundleModels.cs
Support bundle request/result models                                         

src/SwfocTrainer.Core/Models/SupportBundleModels.cs

  • Defines SupportBundleRequest record with output directory, profile ID,
    notes, and max recent runs
  • Defines SupportBundleResult record with bundle path, manifest path,
    included files, and warnings
+20/-0   
IProfileUpdateService.cs
Profile update service contract expansion                               

src/SwfocTrainer.Core/Contracts/IProfileUpdateService.cs

  • Added InstallProfileTransactionalAsync method returning structured
    ProfileInstallResult
  • Added RollbackLastInstallAsync method returning ProfileRollbackResult
+6/-0     
TelemetrySnapshotModels.cs
Telemetry snapshot data model                                                       

src/SwfocTrainer.Core/Models/TelemetrySnapshotModels.cs

  • Defines TelemetrySnapshot record with action success/failure counters,
    address source counters, and computed rates
+14/-0   
ITelemetrySnapshotService.cs
Telemetry snapshot service contract                                           

src/SwfocTrainer.Core/Contracts/ITelemetrySnapshotService.cs

  • Defines interface for recording actions, creating snapshots, exporting
    to JSON, and resetting counters
+17/-0   
IModOnboardingService.cs
Mod onboarding service contract                                                   

src/SwfocTrainer.Core/Contracts/IModOnboardingService.cs

  • Defines interface for scaffolding draft custom-mod profiles from
    launch samples
+11/-0   
ISupportBundleService.cs
Support bundle service contract                                                   

src/SwfocTrainer.Core/Contracts/ISupportBundleService.cs

  • Defines interface for exporting support bundles with diagnostics and
    runtime snapshots
+11/-0   
validate-calibration-artifact.ps1
Calibration artifact schema validation script                       

tools/validate-calibration-artifact.ps1

  • Validates calibration artifact JSON against schema with required field
    checks
  • Validates candidate entries have symbol, source, healthStatus, and
    confidence (0-1 range)
  • Validates optional process metadata structure when present
  • Strict mode requires non-placeholder module fingerprint
+100/-0 
validate-support-bundle-manifest.ps1
Support bundle manifest schema validation script                 

tools/validate-support-bundle-manifest.ps1

  • Validates support bundle manifest JSON against schema with required
    field checks
  • Validates includedFiles array is non-empty with non-empty entries
  • Validates warnings array entries are non-null
  • Strict mode requires manifest.json and runtime-snapshot.json in
    included files
+96/-0   
MainWindow.xaml
M3/M4 UI layout for onboarding and ops features                   

src/SwfocTrainer.App/MainWindow.xaml

  • Added "Mod Compatibility Studio" group with fields for base profile,
    draft profile ID, display name, and launch samples
  • Added buttons for scaffold draft, export calibration artifact, and
    build compatibility report
  • Added "Ops Hardening" group with support bundle and telemetry export
    buttons
  • Reorganized Profiles & Updates tab into three-row layout with update
    feed and onboarding/ops summary panels
+84/-1   
release-portable.yml
GitHub Release publishing with checksum verification         

.github/workflows/release-portable.yml

  • Added workflow inputs for manual tag name, publish release flag, and
    prerelease flag
  • Added SHA256 checksum generation for portable zip artifact
  • Added GitHub Release publish step with conditional logic for
    tag-triggered vs manual dispatch
  • Uploads both zip and checksum to release artifacts; creates or updates
    release with notes template
+74/-4   
calibration-artifact.schema.json
Calibration artifact JSON schema definition                           

tools/schemas/calibration-artifact.schema.json

  • Defines JSON schema for calibration artifacts with required fields
    (schemaVersion, generatedAtUtc, profileId, moduleFingerprint,
    candidates)
  • Validates candidates array with symbol, source, healthStatus, and
    confidence (0-1) fields
  • Validates optional process metadata with pid, name, path,
    commandLineAvailable, launchKind, launchReasonCode
  • Validates operatorNotes as optional string
+114/-0 
sonarcloud.yml
SonarCloud workflow conditional execution                               

.github/workflows/sonarcloud.yml

  • Added sonar-token detection step to gracefully skip SonarCloud scan
    when token is not configured
  • Wrapped all subsequent steps with conditional if: ${{
    steps.sonar-token.outputs.enabled == 'true' }}
  • Logs informational message when SONAR_TOKEN is not available
+18/-1   
Tests
13 files
ProfileUpdateServiceTransactionalTests.cs
Transactional profile update and rollback tests                   

tests/SwfocTrainer.Tests/Profiles/ProfileUpdateServiceTransactionalTests.cs

  • Tests transactional profile install with backup creation and SHA256
    verification
  • Verifies install receipt generation and rollback restoration from
    timestamped backups
  • Uses stub HTTP handler to mock remote manifest and package downloads
  • Validates that profile JSON is updated and can be rolled back to
    previous version
+161/-0 
ModCalibrationServiceTests.cs
Mod calibration service unit tests                                             

tests/SwfocTrainer.Tests/Profiles/ModCalibrationServiceTests.cs

  • Tests compatibility report blocking promotion when critical symbols
    are unresolved
  • Tests calibration artifact export with symbol candidates and module
    fingerprint generation
  • Validates artifact JSON schema compliance and candidate count
+123/-0 
ModOnboardingServiceTests.cs
Mod onboarding service unit tests                                               

tests/SwfocTrainer.Tests/Profiles/ModOnboardingServiceTests.cs

  • Tests draft profile scaffolding from launch samples with workshop ID
    and path hint inference
  • Validates profile ID normalization, namespace handling, and alias
    generation
  • Verifies output profile JSON contains inherited base profile and
    metadata with inferred hints
+108/-0 
SupportBundleServiceTests.cs
Support bundle service unit tests                                               

tests/SwfocTrainer.Tests/Core/SupportBundleServiceTests.cs

  • Tests support bundle export with zip creation and manifest generation
  • Verifies inclusion of runtime snapshot, telemetry, and repro bundle
    files
  • Uses stub runtime adapter and telemetry service to simulate attached
    session
+83/-0   
SaveCorpusRoundTripTests.cs
Save corpus round-trip validation tests                                   

tests/SwfocTrainer.Tests/Saves/SaveCorpusRoundTripTests.cs

  • Tests round-trip codec validation for all shipped save schemas using
    tracked corpus fixtures
  • Loads fixture manifest from tools/fixtures/save-corpus and validates
    each schema
  • Verifies codec can load, validate, and round-trip synthetic save files
+69/-0   
TelemetrySnapshotServiceTests.cs
Telemetry snapshot service unit tests                                       

tests/SwfocTrainer.Tests/Core/TelemetrySnapshotServiceTests.cs

  • Tests action recording with multiple invocations and counter
    aggregation
  • Tests snapshot creation with computed rates (failure, fallback,
    unresolved)
  • Tests JSON export to file with timestamp naming
+44/-0   
ci.yml
CI pipeline schema validation expansion                                   

.github/workflows/ci.yml

  • Added calibration artifact schema smoke test step
  • Added support bundle manifest schema smoke test step
  • Both steps validate sample fixtures against their respective schemas
    with strict mode
+10/-0   
support_bundle_manifest_sample.json
Support bundle manifest test fixture                                         

tools/fixtures/support_bundle_manifest_sample.json

  • New sample fixture for support bundle manifest validation
  • Contains example structure with schema version, timestamp, profile ID,
    and included files
  • Includes sample warnings for testing purposes
+16/-0   
manifest.json
Save corpus manifest for deterministic testing                     

tools/fixtures/save-corpus/manifest.json

  • New manifest file listing four schema fixtures for corpus coverage
  • References base SWEAW, base SWFOC, AOTR, and ROE schema fixtures
+9/-0     
aotr_1397421866_swfoc_v1.json
AOTR schema corpus fixture                                                             

tools/fixtures/save-corpus/aotr_1397421866_swfoc_v1.json

  • New synthetic deterministic corpus fixture for AOTR schema
  • Defines schema ID and synthetic byte length for testing
+5/-0     
roe_3447786229_swfoc_v1.json
ROE schema corpus fixture                                                               

tools/fixtures/save-corpus/roe_3447786229_swfoc_v1.json

  • New synthetic deterministic corpus fixture for ROE schema
  • Defines schema ID and synthetic byte length for testing
+5/-0     
base_swfoc_steam_v1.json
Base SWFOC schema corpus fixture                                                 

tools/fixtures/save-corpus/base_swfoc_steam_v1.json

  • New synthetic deterministic corpus fixture for base SWFOC schema
  • Defines schema ID and synthetic byte length for testing
+5/-0     
base_sweaw_steam_v1.json
Base SWEAW schema corpus fixture                                                 

tools/fixtures/save-corpus/base_sweaw_steam_v1.json

  • New synthetic deterministic corpus fixture for base SWEAW schema
  • Defines schema ID and synthetic byte length for testing
+5/-0     
Configuration changes
4 files
App.xaml.cs
Service registration for M3/M4 features                                   

src/SwfocTrainer.App/App.xaml.cs

  • Registered four new service implementations in DI container:
    IModCalibrationService, ITelemetrySnapshotService,
    ISupportBundleService, IModOnboardingService
+4/-0     
validate-policy-contracts.ps1
Policy contract updates for M3/M4 artifacts                           

tools/validate-policy-contracts.ps1

  • Added checks for new workflow file
    .github/workflows/release-portable.yml
  • Added checks for new documentation files (MOD_ONBOARDING_RUNBOOK.md,
    RELEASE_RUNBOOK.md, release-notes-template.md)
  • Added checks for new schema files (calibration-artifact.schema.json,
    support-bundle-manifest.schema.json)
  • Added validation for release workflow content (portable zip, checksum,
    gh release commands)
+13/-1   
support-bundle-manifest.schema.json
Support bundle manifest schema definition                               

tools/schemas/support-bundle-manifest.schema.json

  • New JSON schema for support bundle manifest validation
  • Defines required fields: schemaVersion, generatedAtUtc, includedFiles,
    warnings
  • Includes optional fields for profileId and notes
+42/-0   
visual-audit.yml
Visual audit workflow environment variable configuration 

.github/workflows/visual-audit.yml

  • Added APPLITOOLS_API_KEY environment variable configuration from
    secrets
  • Updated conditional check to use environment variable instead of
    direct secret reference
+3/-1     
Bug fix
1 files
compare-visual-pack.ps1
Visual pack comparison path normalization fix                       

tools/compare-visual-pack.ps1

  • Fixed relative path calculation to properly trim leading path
    separators using regex replacement
+1/-1     
Documentation
7 files
TODO.md
Execution tracking for M2/M3/M4 completion                             

TODO.md

  • Marked M2 corpus coverage completion as done with test evidence
  • Marked M3 custom-mod onboarding as done with code and test evidence
  • Marked M3 signature calibration as done with code, schema, and test
    evidence
  • Marked M4 profile-pack operational hardening as done with code, test,
    and validation evidence
  • Marked M4 deployment strategy as done with workflow and documentation
    evidence
+23/-4   
RELEASE_RUNBOOK.md
Release workflow and distribution channel documentation   

docs/RELEASE_RUNBOOK.md

  • New release runbook documenting tag-driven and manual dispatch
    workflows for GitHub Releases
  • Includes artifact verification procedures using SHA256 checksums
  • Defines security controls, rollback procedures, and dry-run evidence
    templates
+52/-0   
PROFILE_FORMAT.md
Profile format documentation for M3 onboarding                     

docs/PROFILE_FORMAT.md

  • Added documentation for onboardingNotes metadata field
  • Documented custom namespace convention (M3) for draft profiles with
    custom_ prefix
  • Added references to M3/M4 tooling schemas (calibration artifact and
    support bundle manifest)
+19/-0   
SAVE_LAB_RUNBOOK.md
Save Lab runbook enhancement and safety documentation       

docs/SAVE_LAB_RUNBOOK.md

  • Expanded backup receipt recovery section with fault-tolerance details
  • Added new section on cross-profile compatibility blocking by design
  • Clarified error messages for profile/schema mismatch scenarios
  • Reorganized notes for better clarity
+15/-3   
SYMBOL_CALIBRATION_TODO.md
Calibration evidence payload standardization                         

docs/SYMBOL_CALIBRATION_TODO.md

  • Added standardized evidence payload requirements for closure evidence
  • Defined required fields: runId, classification, profileId, launch
    recommendation, runtime mode
  • Specified artifact paths for test results and repro bundles
+16/-0   
release-notes-template.md
Release notes template for GitHub Releases                             

docs/release-notes-template.md

  • New template for release notes with highlights, included artifacts,
    and known limitations
  • Includes verification instructions using SHA256 checksums
  • Provides rollback guidance referencing the release runbook
+24/-0   
EXTERNAL_TOOLS_SETUP.md
External tools setup for GitHub Releases                                 

docs/EXTERNAL_TOOLS_SETUP.md

  • Added GitHub Releases distribution channel documentation
  • Documented workflow location and produced artifacts
  • References release runbook for operational instructions
+9/-0     
Additional files
3 files
README.md +13/-1   
MOD_ONBOARDING_RUNBOOK.md +51/-0   
calibration_artifact_sample.json +31/-0   

Summary by CodeRabbit

  • New Features

    • Mod Compatibility Studio: scaffold custom profiles, generate calibration artifacts, and produce compatibility reports
    • Ops Hardening: export support bundles and telemetry snapshots; transactional profile installs with rollback
  • Improvements

    • Release flow: portable package publishing, SHA256 artifact checks, and manual release dispatch
    • CI: added artifact/manifest schema smoke validations and safer SonarCloud/Applitools gating
  • Documentation

    • New runbooks, onboarding guides, schema docs, and a release notes template
  • Tests

    • Added unit and integration tests for onboarding, calibration, support bundles, telemetry, and profile updates

@devloai
Copy link

devloai bot commented Feb 18, 2026

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@github-actions github-actions bot added area:app WPF app/UI/ViewModel area:profiles Profile metadata, inheritance, manifest area:ci CI/CD and automation area:tooling Scripts and developer tooling area:docs Documentation/process related work. needs-reviewer No eligible non-author reviewer available labels Feb 18, 2026
@github-actions
Copy link

Reviewer automation could not request a non-author reviewer for this PR.

  • PR author: @Prekzursil
  • configured users: Prekzursil
  • configured teams:
  • fallback label applied: needs-reviewer

Next steps:

  1. Add at least one non-author collaborator/team to config/reviewer-roster.json.
  2. Re-run reviewer automation (or request review manually).

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds mod onboarding, calibration, telemetry, support-bundle export, transactional profile install/rollback, schema validators and CI release automation, plus UI/ViewModel wiring, data models, tests, and documentation updates.

Changes

Cohort / File(s) Summary
Workflows & CI
/.github/workflows/ci.yml, /.github/workflows/release-portable.yml, /.github/workflows/sonarcloud.yml, /.github/workflows/visual-audit.yml
Added calibration-artifact and support-bundle manifest smoke validation steps; introduced release-portable workflow with checksum generation and GitHub release publishing; gated SonarCloud steps via token detection step; bound APPLITOOLS_API_KEY in visual-audit job.
App UI & VM
src/SwfocTrainer.App/MainWindow.xaml, src/SwfocTrainer.App/ViewModels/MainViewModel.cs, src/SwfocTrainer.App/App.xaml.cs
Restructured Profiles & Updates UI; added Mod Compatibility Studio / Ops Hardening controls; injected and registered new services; added many onboarding/calibration state properties and six public commands plus rollback command.
Core Services
src/SwfocTrainer.Core/Services/ModCalibrationService.cs, src/SwfocTrainer.Core/Services/SupportBundleService.cs, src/SwfocTrainer.Core/Services/TelemetrySnapshotService.cs, src/SwfocTrainer.Core/Services/TrainerOrchestrator.cs
Implemented calibration artifact export and compatibility reporting, support-bundle packaging with manifest, telemetry snapshot service with export, and added telemetry recording in orchestrator.
Profiles Services
src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs, src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs
Added ModOnboardingService to scaffold draft profiles from launch samples; extended GitHubProfileUpdateService with transactional InstallProfileTransactionalAsync and RollbackLastInstallAsync plus receipts and guarded failure codes.
Contracts / Interfaces
src/SwfocTrainer.Core/Contracts/...
IModOnboardingService, IModCalibrationService, ISupportBundleService, ITelemetrySnapshotService, IProfileUpdateService
Added interfaces for onboarding, calibration, support bundle, telemetry snapshot; extended IProfileUpdateService with transactional install and rollback methods.
Models
src/SwfocTrainer.Core/Models/...
ModOnboardingModels.cs, CompatibilityReportModels.cs, ProfileUpdateModels.cs, SupportBundleModels.cs, TelemetrySnapshotModels.cs
Added record types for onboarding/calibration requests & results, compatibility reports, profile install/rollback results, support bundle request/result, and telemetry snapshot structure.
Schemas & Validators
tools/schemas/calibration-artifact.schema.json, tools/schemas/support-bundle-manifest.schema.json, tools/validate-calibration-artifact.ps1, tools/validate-support-bundle-manifest.ps1, tools/validation-helpers.ps1
Added JSON schemas for calibration artifacts and support-bundle manifests and PowerShell validators (strict mode, aggregated errors) plus shared validation helpers.
Policy & Tooling
tools/validate-policy-contracts.ps1, tools/compare-visual-pack.ps1, tools/validate-repro-bundle.ps1
Expanded policy checklist to include release workflow and schemas; adjusted visual-pack path handling; refactored repro-bundle validation to use shared validation helpers.
Fixtures & Test Data
tools/fixtures/*, tools/fixtures/save-corpus/*
Added calibration artifact and support bundle sample fixtures; added save-corpus fixtures and manifest for round-trip tests.
Tests
tests/SwfocTrainer.Tests/... (multiple files)
Added tests covering ModOnboardingService, ModCalibrationService, SupportBundleService, TelemetrySnapshotService, transactional profile install/rollback, and save-corpus round-trip.
Docs & Runbooks
README.md, TODO.md, docs/*.md
Added/updated runbooks and docs: release runbook, onboarding runbook, profile format updates, test plan, release notes template, and other operational guidance.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant MainVM as MainViewModel
    participant ModOnboard as ModOnboardingService
    participant ProfileRepo as IProfileRepository
    participant FS as FileSystem

    User->>MainVM: Scaffold Draft Profile(request)
    MainVM->>ModOnboard: ScaffoldDraftProfileAsync(request)
    ModOnboard->>ProfileRepo: Resolve base profile
    ProfileRepo-->>ModOnboard: base profile JSON
    ModOnboard->>ModOnboard: Infer workshop IDs & path hints
    ModOnboard->>FS: Write draft profile JSON
    FS-->>ModOnboard: output path
    ModOnboard-->>MainVM: ModOnboardingResult(profileId, hints)
    MainVM-->>User: Update UI (OnboardingSummary)
Loading
sequenceDiagram
    participant User as User
    participant MainVM as MainViewModel
    participant ProfileSvc as GitHubProfileUpdateService
    participant FS as FileSystem

    User->>MainVM: Install Profile Update(profileId)
    MainVM->>ProfileSvc: InstallProfileTransactionalAsync(profileId)
    ProfileSvc->>ProfileSvc: Fetch manifest & download ZIP
    ProfileSvc->>ProfileSvc: Validate SHA256 & extract
    ProfileSvc->>FS: Create timestamped backup
    FS-->>ProfileSvc: backup path
    ProfileSvc->>FS: Write new profile JSON
    ProfileSvc->>FS: Write install receipt
    ProfileSvc-->>MainVM: ProfileInstallResult(success, paths)

    User->>MainVM: Rollback Profile
    MainVM->>ProfileSvc: RollbackLastInstallAsync(profileId)
    ProfileSvc->>FS: Locate latest backup
    FS-->>ProfileSvc: backup path
    ProfileSvc->>FS: Restore backup, write rollback receipt
    ProfileSvc-->>MainVM: ProfileRollbackResult(restored, paths)
    MainVM-->>User: Display rollback status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

area:runtime, area:saves

Poem

🐰 A soft hop through files and lines,

Drafts and artifacts in tidy designs,
Checksums hum and snapshots sing,
Rollback ready, telemetry in spring,
A carrot-cheer for studio and wings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main objectives: M3/M4 implementation (onboarding, ops hardening, release strategy). It accurately reflects the comprehensive nature of the PR without excessive length or vagueness.
Description check ✅ Passed PR description comprehensively covers scope, affected profiles, reliability evidence, linked issues, and detailed implementation walkthroughs. However, the provided description template requires specific sections (Runtime logic, Validation Evidence, Runtime Safety Checklist, Metadata/Contract Changes) that are not explicitly checked in the author description.
Linked Issues check ✅ Passed All major coding objectives from linked issues are met: M2 corpus coverage (SaveCorpusRoundTripTests + fixtures), M3 onboarding/calibration/compatibility (services, contracts, UI), M4 transactional updates/support bundles/telemetry, release strategy (portable + checksums + runbook), and ops hardening (schemas, validation, CI integration). Issue mapping is complete (#8#10, #29, #39#44).
Out of Scope Changes check ✅ Passed All changes align with stated objectives: contracts/services/tests for M3/M4, UI layout for feature integration, CI/tooling enhancements, documentation, and schemas. Minor visual-pack path fix is a legitimate bug fix. No unrelated or scope-creeping changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/full-issue-closure-m3-m4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 18, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Zip Slip extraction

Description: Untrusted ZIP content is extracted via ZipFile.ExtractToDirectory(zipPath, extractDir)
without validating entry paths, which can enable a Zip Slip path traversal (e.g., ../
entries writing outside extractDir) if the download source is compromised.
GitHubProfileUpdateService.cs [168-171]

Referred Code
try
{
    ZipFile.ExtractToDirectory(zipPath, extractDir);
}
Sensitive bundle contents

Description: The exported support bundle can include sensitive local information (e.g., log contents,
process path/metadata, and potentially user environment details) that may be
unintentionally shared when users upload the ZIP for support.
SupportBundleService.cs [190-242]

Referred Code
private async Task WriteRuntimeSnapshotAsync(
    string stagingRoot,
    List<string> included,
    List<string> warnings,
    string? profileId,
    string? notes,
    CancellationToken cancellationToken)
{
    var path = Path.Combine(stagingRoot, "runtime-snapshot.json");

    if (_runtime.CurrentSession is null)
    {
        var payload = new
        {
            generatedAtUtc = DateTimeOffset.UtcNow,
            profileId,
            notes,
            attached = false
        };

        await File.WriteAllTextAsync(path, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);


 ... (clipped 32 lines)
Ticket Compliance
🟡
🎫 #34
Capture live-machine evidence for tactical toggle live smoke.
Capture live-machine evidence for AOTR/ROE hero helper smoke.
Update docs/TEST_PLAN and docs/SYMBOL_CALIBRATION_TODO to include a standardized reporting
format.
🟡
🎫 #19
Maintain a live-machine calibration checklist for AOTR and ROE profile health
verification.
Checklist includes attach sanity checks.
Checklist includes launch reason code checks.
Checklist includes key action smoke checks.
Standardize the manual evidence format used in issue updates.
🟡
🎫 #29
🟢 Define publishing model, environment hardening, and release governance.
Evaluate GitHub Pages vs release artifacts vs external hosting after CI/security baseline
is stable.
Produce a decision record for deployment target and release channel.
Document security controls for the chosen deployment/publishing path.
Link a rollout checklist from M4.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled async exceptions: New UI command handlers call services that can throw (e.g., onboarding/calibration/support
bundle/telemetry export) without try/catch, risking unhandled exceptions and non-graceful
failure in the UI.

Referred Code
private async Task ScaffoldModProfileAsync()
{
    var launchLines = OnboardingLaunchSample
        .Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
        .ToArray();
    var launchSamples = launchLines
        .Select(line => new ModLaunchSample(ProcessName: null, ProcessPath: null, CommandLine: line))
        .ToArray();

    var request = new ModOnboardingRequest(
        DraftProfileId: OnboardingDraftProfileId,
        DisplayName: OnboardingDisplayName,
        BaseProfileId: string.IsNullOrWhiteSpace(OnboardingBaseProfileId) ? "base_swfoc" : OnboardingBaseProfileId,
        LaunchSamples: launchSamples,
        ProfileAliases: new[] { OnboardingDraftProfileId, OnboardingDisplayName },
        NamespaceRoot: OnboardingNamespaceRoot,
        Notes: "Generated by Mod Compatibility Studio");

    var result = await _modOnboarding.ScaffoldDraftProfileAsync(request);
    var warnings = result.Warnings.Count == 0
        ? "none"


 ... (clipped 64 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Leaky error messages: Error results and UI status strings propagate raw exception messages (e.g.,
HTTP/download/extract failures) that may expose internal paths/URLs/implementation details
to end users.

Referred Code
ProfileManifest? remote;
try
{
    remote = await _httpClient.GetFromJsonAsync<ProfileManifest>(_options.RemoteManifestUrl, _jsonOptions, cancellationToken);
}
catch (Exception ex)
{
    return new ProfileInstallResult(
        Succeeded: false,
        ProfileId: profileId,
        InstalledPath: string.Empty,
        BackupPath: null,
        ReceiptPath: null,
        Message: $"Failed to fetch remote manifest: {ex.Message}",
        ReasonCode: "manifest_fetch_failed");
}

if (remote is null)
{
    return new ProfileInstallResult(
        Succeeded: false,


 ... (clipped 78 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing user context: New telemetry/audit recording captures action ID and outcome but does not include an
operator/user identifier, so it is unclear whether audit trail requirements are met for
user attribution.

Referred Code
private readonly ITelemetrySnapshotService _telemetry;

public TrainerOrchestrator(
    IProfileRepository profiles,
    IRuntimeAdapter runtime,
    IValueFreezeService freezeService,
    IAuditLogger auditLogger,
    ITelemetrySnapshotService? telemetry = null)
{
    _profiles = profiles;
    _runtime = runtime;
    _freezeService = freezeService;
    _auditLogger = auditLogger;
    _telemetry = telemetry ?? new TelemetrySnapshotService();
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Sensitive data export: Support bundle export packages log files and writes runtime-snapshot.json containing
ProcessPath and potentially command-line related metadata, which may capture sensitive
user/system information depending on environment.

Referred Code
private static void CopyLogs(string stagingRoot, List<string> included, List<string> warnings)
{
    var logsRoot = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "SwfocTrainer",
        "logs");

    if (!Directory.Exists(logsRoot))
    {
        warnings.Add("Log directory not found in LocalAppData.");
        return;
    }

    var targetRoot = Path.Combine(stagingRoot, "logs");
    Directory.CreateDirectory(targetRoot);
    foreach (var path in Directory
                 .GetFiles(logsRoot, "*.jsonl", SearchOption.TopDirectoryOnly)
                 .OrderByDescending(File.GetLastWriteTimeUtc)
                 .Take(10))
    {
        var fileName = Path.GetFileName(path);


 ... (clipped 125 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated output path: The user-controlled SupportBundleOutputDirectory is used to create directories and write
artifacts without additional validation or allowlisting, which may be acceptable for a
local desktop app but requires confirmation of the threat model and expected permissions.

Referred Code
private string _supportBundleOutputDirectory = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "SwfocTrainer",
    "support");
private SpawnPresetViewItem? _selectedSpawnPreset;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codacy-production
Copy link

codacy-production bot commented Feb 18, 2026

Codacy's Analysis Summary

69 new issues (≤ 0 issue)
1 new security issue
159 complexity
2 duplications

Review Pull Request in Codacy →

AI Reviewer available: add the codacy-review label to get contextual insights without leaving GitHub.

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 18, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Handle zip errors gracefully

Wrap the ZipFile.CreateFromDirectory call in a try-catch block to handle
potential exceptions gracefully and return a failed SupportBundleResult instead
of throwing an unhandled exception.

src/SwfocTrainer.Core/Services/SupportBundleService.cs [79-86]

-ZipFile.CreateFromDirectory(stagingRoot, bundlePath, CompressionLevel.Optimal, includeBaseDirectory: false);
+try
+{
+    ZipFile.CreateFromDirectory(stagingRoot, bundlePath, CompressionLevel.Optimal, includeBaseDirectory: false);
+}
+catch (Exception ex)
+{
+    return new SupportBundleResult(
+        Succeeded: false,
+        BundlePath: string.Empty,
+        ManifestPath: manifestPath,
+        IncludedFiles: Array.Empty<string>(),
+        Warnings: warnings.Append($"zip_failed:{ex.Message}").ToArray());
+}
 return new SupportBundleResult(
     Succeeded: true,
     BundlePath: bundlePath,
     ManifestPath: manifestPath,
     IncludedFiles: included.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray(),
     Warnings: warnings);
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly points out that an exception during zip creation would crash the operation, and wrapping it in a try-catch block provides more graceful error handling and a clearer failure result.

Medium
Enforce non-empty unique file list

Enhance the includedFiles schema by adding "minItems": 1 and "uniqueItems":
true. This ensures the file list is never empty and contains no duplicate
entries.

tools/schemas/support-bundle-manifest.schema.json [28-34]

 "includedFiles": {
   "type": "array",
+  "minItems": 1,
+  "uniqueItems": true,
   "items": {
     "type": "string",
     "minLength": 1
   }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: This suggestion correctly identifies an opportunity to strengthen the schema by ensuring the includedFiles array is not empty and contains no duplicates, which improves the reliability of the support bundle.

Low
Prevent empty strings in warnings

Add "minLength": 1 to the items within the warnings array schema. This will
prevent empty strings from being considered valid warnings, thus improving data
integrity.

tools/schemas/support-bundle-manifest.schema.json [35-40]

 "warnings": {
   "type": "array",
   "items": {
-    "type": "string"
+    "type": "string",
+    "minLength": 1
   }
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: This suggestion improves schema robustness by preventing empty warning strings, which enhances data quality and consistency with other parts of the schema like includedFiles.

Low
Possible issue
Use atomic file move for installation

Replace the temporary file copy-and-delete logic with a single, atomic File.Move
operation to install the profile file, making the update process more resilient.

src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs [236-248]

-var tempInstallPath = $"{destination}.tmp";
-File.Copy(targetProfileJson, tempInstallPath, overwrite: true);
-try
-{
-    File.Copy(tempInstallPath, destination, overwrite: true);
-}
-finally
-{
-    if (File.Exists(tempInstallPath))
-    {
-        File.Delete(tempInstallPath);
-    }
-}
+File.Move(targetProfileJson, destination, overwrite: true);
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a non-atomic file write operation and proposes using File.Move for a more resilient, atomic replacement, which simplifies the code and improves robustness.

Medium
Handle all newline variations robustly

Replace Environment.NewLine with an array of common newline sequences ("\r\n",
"\r", "\n") when splitting the OnboardingLaunchSample string to handle all
line ending variations.

src/SwfocTrainer.App/ViewModels/MainViewModel.cs [1221-1223]

 var launchLines = OnboardingLaunchSample
-    .Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
+    .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
     .ToArray();
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that Environment.NewLine is insufficient for parsing multi-line text from a UI TextBox and proposes a more robust method that handles various line endings.

Low
Security
Improve security by scoping secrets

To improve security, revert the change that made APPLITOOLS_API_KEY a job-level
environment variable. Instead, use the secrets context directly in the if
condition to limit the secret's scope and reduce potential exposure.

.github/workflows/visual-audit.yml [24-65]

 jobs:
   compare-visual-pack:
     runs-on: windows-latest
-    env:
-      APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
     steps:
       - name: Checkout
         uses: actions/checkout@v4
 ...
       - name: Applitools availability note
-        if: ${{ env.APPLITOOLS_API_KEY != '' }}
+        if: ${{ secrets.APPLITOOLS_API_KEY != '' }}
         shell: pwsh
         run: |
           Write-Host "APPLITOOLS_API_KEY is configured. Visual pack can be uploaded for external review."

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This is a valid security hardening suggestion that correctly reverts a change from the PR, aligning with the principle of least privilege by reducing the exposure of a secret.

Medium
  • Update

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b10f239328

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

generatedAtUtc = DateTimeOffset.UtcNow,
profileId = request.ProfileId,
notes = request.Notes,
includedFiles = included.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add manifest.json before serializing includedFiles

ExportAsync builds manifestPayload from includedFiles before manifest.json is appended to included, so every emitted manifest omits itself from the inventory even though the bundle later contains it. This breaks the support-bundle contract introduced here (validate-support-bundle-manifest.ps1 -Strict requires manifest.json in includedFiles) and makes produced manifests fail strict validation.

Useful? React with 👍 / 👎.

Comment on lines +237 to +238
ExportCalibrationArtifactCommand = new AsyncCommand(ExportCalibrationArtifactAsync, () => !string.IsNullOrWhiteSpace(SelectedProfileId));
BuildCompatibilityReportCommand = new AsyncCommand(BuildCompatibilityReportAsync, () => !string.IsNullOrWhiteSpace(SelectedProfileId));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run onboarding exports against the drafted profile

The onboarding commands are gated on SelectedProfileId being non-empty, but the new onboarding export/report methods are written to fall back to OnboardingDraftProfileId; this guard makes that fallback unreachable and causes Mod Compatibility Studio actions to run against the currently selected manifest profile instead of the just-scaffolded draft profile. In the documented onboarding flow, that produces calibration/report evidence for the wrong profile.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (28)
docs/release-notes-template.md (1)

15-21: Add an explicit hash comparison to the verification snippet.

The current snippet prints the computed hash and the stored hash separately, requiring the reader to compare them visually. An explicit comparison reduces the chance of human error during release verification.

📋 Suggested verification snippet
 ## Verification
 - Validate checksum before use:
 
 ```powershell
 Get-FileHash .\SwfocTrainer-portable.zip -Algorithm SHA256
 Get-Content .\SwfocTrainer-portable.zip.sha256
+# Compare — must match:
+$computed = (Get-FileHash .\SwfocTrainer-portable.zip -Algorithm SHA256).Hash
+$expected = (Get-Content .\SwfocTrainer-portable.zip.sha256).Trim().Split(' ')[0].ToUpper()
+if ($computed -ne $expected) { throw "Checksum mismatch: $computed vs $expected" }
+Write-Host "Checksum OK: $computed"
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/release-notes-template.md around lines 15 - 21, The verification
snippet currently prints hashes separately; update the block that uses
Get-FileHash and Get-Content to compute and compare the values programmatically
by assigning the computed hash to a variable (e.g., $computed = (Get-FileHash
.\SwfocTrainer-portable.zip -Algorithm SHA256).Hash), read and normalize the
stored value into another variable (e.g., $expected from Get-Content and
Trim()/Split()), then perform an explicit comparison (if ($computed -ne
$expected) { throw "Checksum mismatch..." }) and emit a clear success message
(e.g., Write-Host "Checksum OK: $computed") so users don’t have to compare
values manually; modify the snippet containing Get-FileHash and Get-Content
accordingly.


</details>

</blockquote></details>
<details>
<summary>tools/fixtures/save-corpus/base_swfoc_steam_v1.json (1)</summary><blockquote>

`1-5`: **Consider schema-representative `syntheticByteLength` values across fixtures.**

Profile naming is correct and complies with guidelines. However, all four corpus fixtures share the same `syntheticByteLength: 300000`. If `SaveCorpusRoundTripTests` feeds this as a typed payload through the per-schema codec (rather than raw byte passthrough), uniform sizes reduce the chance of catching schema-specific framing bugs (e.g., fixed-size headers, minimum record counts, or alignment constraints that differ between SWEAW, SWFOC, AOTR, and ROE formats).

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @tools/fixtures/save-corpus/base_swfoc_steam_v1.json around lines 1 - 5, The
fixture for schemaId "base_swfoc_steam_v1" uses a generic syntheticByteLength
(300000) shared across multiple corpus fixtures; update this JSON to use a
schema-representative syntheticByteLength that reflects SWFOC framing/record
expectations (e.g., a value exercising any fixed headers, minimum record counts,
or alignment constraints unique to SWFOC) and ensure the other fixtures for
SWEAW, AOTR, and ROE use different, distinct lengths so SaveCorpusRoundTripTests
exercise schema-specific sizes and catch framing bugs.


</details>

</blockquote></details>
<details>
<summary>.github/workflows/visual-audit.yml (1)</summary><blockquote>

`27-28`: **Scope `APPLITOOLS_API_KEY` to the step that needs it, not the whole job.**

The env context pattern (`env.APPLITOOLS_API_KEY != ''`) in the `if:` condition is correct and more reliable than checking `secrets.*` directly in a step condition. However, declaring the secret at job level exposes it to every step's process environment (Checkout, path resolution, visual-compare, artifact upload) when only the informational note step uses it. The informational step itself is also a stub — it only prints a message and never actually reads the key value.

Scoping the env declaration to the one step that references it reduces the blast radius if any other step in this job is ever replaced with an untrusted action:

<details>
<summary>🔒 Proposed scoping change</summary>

```diff
-    env:
-      APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
     steps:
       - name: Checkout
         uses: actions/checkout@v4
       # ... other steps ...
       - name: Applitools availability note
-        if: ${{ env.APPLITOOLS_API_KEY != '' }}
+        if: ${{ secrets.APPLITOOLS_API_KEY != '' }}
+        env:
+          APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
         shell: pwsh
         run: |
           Write-Host "APPLITOOLS_API_KEY is configured. Visual pack can be uploaded for external review."

If a real Applitools upload step is added later, apply the same step-level pattern there.

Also applies to: 61-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/visual-audit.yml around lines 27 - 28, Move the
APPLITOOLS_API_KEY environment variable out of the job-level env block and
declare it only on the specific step(s) that require it (e.g., the informational
"note" step that checks env.APPLITOOLS_API_KEY and any real Applitools upload
step you add later); keep using the condition pattern env.APPLITOOLS_API_KEY !=
'' in those step-level if: checks so the secret is only injected into the
process environment of the step(s) that actually reference it and not exposed to
other steps like checkout, path resolution, visual-compare, or artifact upload.
tools/fixtures/save-corpus/manifest.json (1)

1-9: Consider adding a $schema reference for tooling-backed validation.

All four fixture paths are present in the repo and profile names (base, aotr, roe) comply with guidelines. Adding a $schema pointer would let the CI schema-smoke checks that already exist in this PR validate the manifest structure automatically, consistent with "Script outputs must be stable and schema-backed where practical."

🔧 Suggested addition
 {
+  "$schema": "../schemas/save-corpus-manifest.schema.json",
   "schemaVersion": "1.0",
   "fixtures": [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/fixtures/save-corpus/manifest.json` around lines 1 - 9, Add a $schema
pointer to the manifest JSON so CI/schema-backed tooling can validate it: update
the manifest object (where "schemaVersion" and "fixtures" are defined in
manifest.json) to include a top-level "$schema" property referencing the
canonical JSON Schema for these fixtures; ensure the $schema value matches the
repo/tooling schema used by other manifests so the CI smoke checks validate this
file automatically.
tools/schemas/support-bundle-manifest.schema.json (1)

35-40: warnings items allow empty strings — inconsistent with includedFiles.

includedFiles items enforce "minLength": 1 (Line 32), but warnings items have no such constraint. If empty-string warning entries should be rejected for consistency, add the same constraint.

♻️ Proposed fix
     "warnings": {
       "type": "array",
       "items": {
-        "type": "string"
+        "type": "string",
+        "minLength": 1
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/schemas/support-bundle-manifest.schema.json` around lines 35 - 40, The
schema's "warnings" array currently allows empty strings whereas "includedFiles"
enforces non-empty entries; update the "warnings" definition (the "warnings"
property and its "items" schema) to include "minLength": 1 so each warning
string must be non-empty, matching the behavior of "includedFiles".
src/SwfocTrainer.Core/Models/SupportBundleModels.cs (1)

15-20: Clarify BundlePath/ManifestPath semantics when Succeeded = false.

Both properties are non-nullable string, but on a failed export they will presumably be empty or meaningless. Callers checking only BundlePath without first testing Succeeded may act on stale or empty paths. Consider making them string? to make the failure contract explicit, or at minimum add XML doc comments documenting the convention.

♻️ Option A — nullable paths on failure
 public sealed record SupportBundleResult(
     bool Succeeded,
-    string BundlePath,
-    string ManifestPath,
+    string? BundlePath,
+    string? ManifestPath,
     IReadOnlyList<string> IncludedFiles,
     IReadOnlyList<string> Warnings);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Models/SupportBundleModels.cs` around lines 15 - 20,
The SupportBundleResult record's BundlePath and ManifestPath should explicitly
allow null to signal meaningless values when Succeeded is false: change
SupportBundleResult to use string? for BundlePath and ManifestPath (i.e.,
SupportBundleResult(... string? BundlePath, string? ManifestPath ...)) and
update any callers that construct or read SupportBundleResult to handle null
(check Succeeded before using paths or null-coalesce appropriately);
alternatively, if you prefer not to change types, add XML doc comments on
SupportBundleResult and its BundlePath/ManifestPath properties clearly stating
they are undefined when Succeeded is false—referencing the SupportBundleResult
type and its BundlePath, ManifestPath and Succeeded members so reviewers can
find the change.
tests/SwfocTrainer.Tests/Saves/SaveCorpusRoundTripTests.cs (1)

18-64: Consider [Theory] + [MemberData] for per-fixture test isolation.

The current single [Fact] iterates all fixtures in a loop. If fixture N fails, xUnit reports one failing test with no information about fixtures N+1..M. Using [Theory] gives a separate result per fixture and makes failures easier to triage.

♻️ Sketch of parameterised approach
+    public static IEnumerable<object[]> FixtureNames()
+    {
+        var root = TestPaths.FindRepoRoot();
+        var manifestPath = Path.Combine(root, "tools", "fixtures", "save-corpus", "manifest.json");
+        if (!File.Exists(manifestPath)) yield break;
+        var manifest = JsonSerializer.Deserialize<SaveCorpusManifest>(
+            File.ReadAllText(manifestPath), JsonOptions);
+        foreach (var name in manifest?.Fixtures ?? [])
+            yield return [name];
+    }
+
+    [Theory]
+    [MemberData(nameof(FixtureNames))]
-    [Fact]
-    public async Task Codec_ShouldRoundTripForShippedSchemaCorpusFixtures()
+    public async Task Codec_ShouldRoundTripForShippedSchemaCorpusFixtures(string fixtureName)
     {
         var root = TestPaths.FindRepoRoot();
         var fixtureDir = Path.Combine(root, "tools", "fixtures", "save-corpus");
-        var manifestPath = Path.Combine(fixtureDir, "manifest.json");
-        File.Exists(manifestPath).Should().BeTrue(...);
-        var manifest = JsonSerializer.Deserialize<SaveCorpusManifest>(...);
-        ...
-        foreach (var fixtureName in manifest.Fixtures)
-        {
             var fixturePath = Path.Combine(fixtureDir, fixtureName);
             // ... rest of per-fixture logic unchanged ...
-        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/SwfocTrainer.Tests/Saves/SaveCorpusRoundTripTests.cs` around lines 18 -
64, Replace the single-loop Fact with a parameterized Theory so each fixture
runs as its own test: add a static MemberData provider (e.g., a public static
IEnumerable<object[]> FixtureManifests or GetFixtureNames method) that reads the
manifest.json (using SaveCorpusManifest / manifest.Fixtures) and yields each
fixture name, change the test attribute on
Codec_ShouldRoundTripForShippedSchemaCorpusFixtures to [Theory] and
[MemberData(nameof(FixtureManifests))], change the test signature to accept a
single string fixtureName and remove the foreach loop, and keep the existing
per-fixture temp file creation and BinarySaveCodec (and SaveOptions) logic
inside the body so failures report per-fixture.
src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs (3)

146-157: Downloaded zip is not cleaned up on SHA mismatch or subsequent failures.

When the SHA check fails (or extraction/validation fails later), the downloaded zip at zipPath remains on disk. Over time, repeated failed installs will accumulate orphaned files in the download cache. Consider deleting the zip in failure paths or adding a general cleanup in a finally block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs` around
lines 146 - 157, The SHA-mismatch and other failure paths in
GitHubProfileUpdateService (where ComputeSha256 is called and a
ProfileInstallResult with Succeeded: false is returned) leave the downloaded zip
at zipPath on disk; update the method to ensure zipPath is deleted on all
error/early-return paths—either delete the file before returning in the SHA
mismatch branch and other failure branches (extraction/validation) or wrap the
download/extract logic in a try/finally that removes zipPath in the finally
block; reference ComputeSha256, zipPath, and the ProfileInstallResult return
sites when making the change.

322-347: Receipt timestamps may drift from backup timestamps.

DateTimeOffset.UtcNow is called independently in both the install flow (Line 228 for backup naming) and in WriteInstallReceiptAsync (Line 332 and Line 337). The receipt generatedAtUtc and receipt filename timestamp may differ from the backup filename timestamp by seconds. This is cosmetic but could cause confusion when correlating receipts to backups. Consider capturing the timestamp once at the top of the transaction and threading it through.

Also applies to: 349-370

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs` around
lines 322 - 347, The receipt timestamp is generated separately inside
WriteInstallReceiptAsync (and the similar method around the other receipt code),
causing potential drift versus the backup filename timestamp; fix this by
generating a single DateTimeOffset timestamp at the start of the
install/uninstall transaction and pass that timestamp into
WriteInstallReceiptAsync (and the corresponding receipt-writing method) as an
extra parameter (e.g., generatedAtUtc) so both the receipt payload property
generatedAtUtc and the receipt filename use the exact same timestamp value.

236-248: File.Copy to destination is not atomic — prefer File.Move with overwrite.

The temp-file pattern here copies source→tmp then tmp→destination via File.Copy. On Windows/NTFS, File.Copy is not atomic and can leave a partially-written destination on crash. File.Move(src, dst, overwrite: true) (available since .NET Core 3.0) performs a rename which is much closer to atomic on the same volume.

♻️ Proposed fix
         var tempInstallPath = $"{destination}.tmp";
         File.Copy(targetProfileJson, tempInstallPath, overwrite: true);
-        try
-        {
-            File.Copy(tempInstallPath, destination, overwrite: true);
-        }
-        finally
-        {
-            if (File.Exists(tempInstallPath))
-            {
-                File.Delete(tempInstallPath);
-            }
-        }
+        File.Move(tempInstallPath, destination, overwrite: true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Profiles/Services/GitHubProfileUpdateService.cs` around
lines 236 - 248, Replace the non-atomic File.Copy(tempInstallPath, destination,
overwrite: true) with File.Move(tempInstallPath, destination, overwrite: true)
so the tmp→destination step uses an atomic rename; keep the initial
File.Copy(targetProfileJson, tempInstallPath, overwrite: true) to create
tempInstallPath, then in the try use File.Move(tempInstallPath, destination,
overwrite: true) and retain the existing finally cleanup that deletes
tempInstallPath if it still exists (ensuring operations occur on the same
volume).
src/SwfocTrainer.Core/Services/TrainerOrchestrator.cs (1)

97-98: Consider wrapping telemetry recording defensively.

RecordAction currently cannot throw in practice (the implementation guards against null actionId and uses simple dictionary operations), but telemetry should never break the primary execution path. A bare try-catch would make this guarantee explicit and protect against future changes to the telemetry service.

🛡️ Defensive wrapping
-        _telemetry.RecordAction(actionId, result.AddressSource, result.Succeeded);
+        try
+        {
+            _telemetry.RecordAction(actionId, result.AddressSource, result.Succeeded);
+        }
+        catch
+        {
+            // Telemetry must never fail the primary action path.
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Services/TrainerOrchestrator.cs` around lines 97 - 98,
Wrap the telemetry call defensively so telemetry failures cannot affect the main
flow: surround the call to _telemetry.RecordAction(actionId,
result.AddressSource, result.Succeeded) in a try-catch inside
TrainerOrchestrator (the method containing this return), swallow or log the
exception (use the existing logger, e.g. _logger?.LogWarning/LogError) and
ensure the original result is still returned; do not change return behavior or
throw from the catch block.
src/SwfocTrainer.Core/Contracts/IProfileUpdateService.cs (1)

9-13: Consider retiring or wrapping InstallProfileAsync to align return types.

InstallProfileAsync returns a bare Task<string> while the new transactional method returns a structured Task<ProfileInstallResult>. If the old method is kept only for backward compatibility, a deprecation note ([Obsolete]) would clarify intent. If both are long-lived, the inconsistency in result type will create confusion at call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Contracts/IProfileUpdateService.cs` around lines 9 -
13, The InstallProfileAsync method returns a raw Task<string> while the newer
InstallProfileTransactionalAsync returns Task<ProfileInstallResult>, creating an
inconsistent API; either mark InstallProfileAsync with [Obsolete] and update
callers to use InstallProfileTransactionalAsync, or change InstallProfileAsync
to return Task<ProfileInstallResult> (or implement a wrapper that calls
InstallProfileTransactionalAsync and maps/returns the string-friendly value) so
both methods produce consistent result types; update the interface
IProfileUpdateService accordingly and adjust implementations of
InstallProfileAsync, InstallProfileTransactionalAsync, and any callers to use
the new/obsolete contract.
tests/SwfocTrainer.Tests/Profiles/ModOnboardingServiceTests.cs (1)

11-12: Consider adding negative-path coverage for InvalidDataException cases.

ModOnboardingService.ScaffoldDraftProfileAsync throws InvalidDataException for empty DraftProfileId, null/empty DisplayName, and missing LaunchSamples. A few [Theory]/[InlineData] cases covering these would harden the contract, especially since the service docs call these out explicitly.

♻️ Sketch for additional negative-path tests
+    [Theory]
+    [InlineData("", "My Mod", "base_swfoc")]         // empty DraftProfileId
+    [InlineData("   ", "My Mod", "base_swfoc")]      // whitespace-only
+    [InlineData("valid_id", "", "base_swfoc")]        // empty DisplayName
+    public async Task ScaffoldDraftProfileAsync_InvalidInput_ShouldThrow(
+        string draftId, string displayName, string baseId)
+    {
+        // minimal setup omitted for brevity — reuse tempRoot pattern
+        var request = new ModOnboardingRequest(
+            DraftProfileId: draftId,
+            DisplayName: displayName,
+            BaseProfileId: baseId,
+            LaunchSamples: Array.Empty<ModLaunchSample>(),  // also covers empty samples
+            ProfileAliases: Array.Empty<string>(),
+            NamespaceRoot: "custom");
+
+        var act = async () => await service.ScaffoldDraftProfileAsync(request);
+        await act.Should().ThrowAsync<InvalidDataException>();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/SwfocTrainer.Tests/Profiles/ModOnboardingServiceTests.cs` around lines
11 - 12, Add negative-path tests for
ModOnboardingService.ScaffoldDraftProfileAsync by creating a new [Theory] in
ModOnboardingServiceTests (or ModOnboardingServiceTests.cs) with [InlineData]
rows that cover empty DraftProfileId, null and empty DisplayName, and
missing/empty LaunchSamples; for each input construct the request payload that
triggers the invalid condition and assert
Assert.ThrowsAsync<InvalidDataException>(() =>
service.ScaffoldDraftProfileAsync(...)) to verify the method throws the expected
exception for those cases.
docs/RELEASE_RUNBOOK.md (1)

37-37: Document or reference the GitHub tag protection configuration backing this immutability claim.

"Tags for published releases are immutable by policy" is a correctness guarantee, but it only holds if tag protection rules are enforced in the GitHub repository settings. Consider linking to the repository's branch/tag protection configuration or noting the required setting (e.g., "Protected tags: v*") so operators can verify or restore the enforcement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/RELEASE_RUNBOOK.md` at line 37, Update the sentence "Tags for published
releases are immutable by policy" to cite or link the concrete GitHub tag
protection settings that enforce this (or add a short note describing the
required protection), e.g., reference the repository's tag protection rules or
include the required rule pattern and options (for example "Protected tags: v*;
prevent deletions and force-pushes"), and add a pointer or checklist so
operators can verify/restore the enforcement in the repo settings.
tests/SwfocTrainer.Tests/Profiles/ProfileUpdateServiceTransactionalTests.cs (1)

55-70: HttpClient wrapping the stub handler is not disposed.

The HttpClient at line 60 and the StubHttpMessageHandler are both IDisposable but never disposed. In a test context this won't cause failures, but if the service holds onto the client, it could delay handler finalization.

💡 Suggested cleanup
-            var handler = new StubHttpMessageHandler(new Dictionary<string, (string ContentType, byte[] Body)>
+            using var handler = new StubHttpMessageHandler(new Dictionary<string, (string ContentType, byte[] Body)>
             {
                 ["https://example.invalid/manifest.json"] = ("application/json", Encoding.UTF8.GetBytes(manifestJson)),
                 [$"https://example.invalid/{profileId}.zip"] = ("application/zip", zipBytes)
             });
-            var client = new HttpClient(handler);
+            using var client = new HttpClient(handler);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/SwfocTrainer.Tests/Profiles/ProfileUpdateServiceTransactionalTests.cs`
around lines 55 - 70, The test creates a StubHttpMessageHandler and an
HttpClient (used to construct GitHubProfileUpdateService) but never disposes
them; update the test to dispose both by scoping the StubHttpMessageHandler and
HttpClient in using blocks or a try/finally that calls Dispose on the handler
and client after creating service (reference StubHttpMessageHandler, new
HttpClient(...), and GitHubProfileUpdateService) so the handler and HttpClient
are properly cleaned up when the test completes.
src/SwfocTrainer.App/MainWindow.xaml (1)

437-438: Consider using a ComboBox for Base Profile to constrain input to supported profiles.

The OnboardingBaseProfileId field is a free-text TextBox, allowing users to type arbitrary values. Since the base profile should be one of the supported profiles (base_sweaw, base_swfoc, aotr_*, roe_*), a ComboBox bound to the existing Profiles collection (or a filtered subset) would reduce user error and keep profile compatibility explicit.

💡 Suggested change
-                                <TextBox Grid.Column="1" Text="{Binding OnboardingBaseProfileId}" Margin="0,0,0,6"/>
+                                <ComboBox Grid.Column="1" ItemsSource="{Binding Profiles}" SelectedItem="{Binding OnboardingBaseProfileId}" IsEditable="True" Margin="0,0,0,6"/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.App/MainWindow.xaml` around lines 437 - 438, The
OnboardingBaseProfileId is currently a free-text TextBox; replace it with a
non-editable ComboBox bound to the existing Profiles collection (or a filtered
subset of supported profiles) so users can only pick valid profiles. Bind the
ComboBox's SelectedValue (or SelectedItem) to OnboardingBaseProfileId, set
DisplayMemberPath to the profile display property and SelectedValuePath to the
profile id property, and ensure the DataContext exposes Profiles (or the
filtered collection) used by the ComboBox; make the ComboBox non-editable to
prevent arbitrary text entry.
src/SwfocTrainer.Core/Models/ProfileUpdateModels.cs (1)

6-13: Clean result record design with reason codes.

The records are well-structured. One observation: InstalledPath and RestoredPath are non-nullable, which means callers must supply a value even on failure paths (e.g., Succeeded = false). If the service always knows the intended target path, this is fine. Otherwise, consider making these nullable to avoid empty-string sentinels on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Models/ProfileUpdateModels.cs` around lines 6 - 13, The
ProfileInstallResult record currently requires InstalledPath (and similar
non-nullable path properties) even on failure paths; change InstalledPath (and
any other path properties that may be unknown on failure) to nullable (string?)
so callers don’t need to pass empty-string sentinels when Succeeded is false;
update the record declaration (ProfileInstallResult) to make InstalledPath
nullable and audit related records/constructors that create ProfileInstallResult
to handle null path values appropriately.
src/SwfocTrainer.Core/Contracts/IModCalibrationService.cs (1)

12-17: BuildCompatibilityReportAsync also lacks an XML comment and has a wide parameter list.

Same XML doc warning applies here. Additionally, the method takes 5 parameters — this is manageable given three are optional with defaults, but if more are added later, consider grouping into a request object.

💡 Suggested fix (XML doc)
+    /// <summary>
+    /// Builds a compatibility report evaluating the given profile against current session and catalog state.
+    /// </summary>
     Task<ModCompatibilityReport> BuildCompatibilityReportAsync(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Contracts/IModCalibrationService.cs` around lines 12 -
17, Add an XML documentation comment for the BuildCompatibilityReportAsync
method on IModCalibrationService describing its purpose, parameters
(TrainerProfile profile, AttachSession? session, DependencyValidationResult?
dependencyValidation, IReadOnlyDictionary<string,IReadOnlyList<string>>?
catalog, CancellationToken cancellationToken) and return value
(Task<ModCompatibilityReport>), and then refactor the signature by introducing a
single request/DTO (e.g., ModCompatibilityRequest) that encapsulates profile,
session, dependencyValidation, and catalog to replace the multiple parameters in
BuildCompatibilityReportAsync while keeping CancellationToken separate; update
the interface method to accept the new request type and update any
implementations accordingly.
tools/validate-calibration-artifact.ps1 (2)

22-49: Add-Error and Require-Field are duplicated across both validation scripts.

Both validate-support-bundle-manifest.ps1 and validate-calibration-artifact.ps1 define identical Add-Error and Require-Field functions. Consider extracting them into a shared helper script (e.g., tools/validation-helpers.ps1) and dot-sourcing it from both scripts to reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` around lines 22 - 49, The Add-Error
and Require-Field functions are duplicated in both validation scripts; extract
these helpers into a shared script (e.g., tools/validation-helpers.ps1) that
defines Add-Error and Require-Field, then replace the duplicated definitions in
validate-calibration-artifact.ps1 and validate-support-bundle-manifest.ps1 with
a dot-source import of that helper. Ensure the shared script exports the same
function names (Add-Error, Require-Field) and that any script-scoped variables
(like $script:errors) remain accessible or are refactored so both callers work
after dot-sourcing.

51-61: Redundant required-field checks: schema-driven loop + hardcoded list.

Lines 51–53 iterate $schema.required to validate fields, then Lines 59–61 re-validate a hardcoded list that likely overlaps with the schema. If schema.required already covers generatedAtUtc, profileId, moduleFingerprint, and candidates, the second loop is pure duplication. If they differ, the intent should be documented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` around lines 51 - 61, The script
currently performs duplicate required-field validation: it iterates
$schema.required with Require-Field and then re-checks a hardcoded list
("generatedAtUtc","profileId","moduleFingerprint","candidates"). Either remove
the hardcoded loop (rely on $schema.required) or explicitly merge the lists and
deduplicate before calling Require-Field; for example, replace the second loop
with building a combined list like ($schema.required + @(...)) | Select-Object
-Unique and iterate that, and add a short comment explaining why extra hardcoded
fields are included if they differ from the schema; keep the existing
schemaVersion check intact.
tools/validate-support-bundle-manifest.ps1 (1)

88-96: Write-Host usage is acceptable for this CI script, but consider Write-Information for broader compatibility.

PSScriptAnalyzer flags Write-Host because it can't be captured or redirected. For a CI validation script that targets colored terminal output, this is a pragmatic choice. If you ever need to consume this script's output programmatically (e.g., piping to a log aggregator), switching to Write-Information with -InformationAction Continue would help. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-support-bundle-manifest.ps1` around lines 88 - 96, The script
currently uses Write-Host in the exit and success paths (the block that checks
$errors.Count and the final Write-Host "support bundle manifest validation
passed: $ManifestPath"), which PSScriptAnalyzer flags because output can't be
redirected; replace those Write-Host calls with Write-Information (and add
-InformationAction Continue where appropriate) so messages can be captured
programmatically while preserving colored output for interactive terminals, and
keep the existing $errors iteration and exit 1 behavior the same.
src/SwfocTrainer.Core/Contracts/ITelemetrySnapshotService.cs (1)

8-17: Clean contract — consider a failure reason parameter for richer diagnostics.

The interface is well-structured and the method signatures are clear. One thing to consider: RecordAction captures success/failure but doesn't carry a failure reason code. If downstream diagnostics or drift analysis need to distinguish why actions fail (e.g., "signature_missing", "address_stale"), adding an optional string? reasonCode = null parameter would make snapshots more actionable without breaking existing callers.

This is non-blocking given that the current counters already provide useful aggregate signals. Based on learnings: "Record actionable reason codes in diagnostics for runtime operations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Contracts/ITelemetrySnapshotService.cs` around lines 8
- 17, Add an optional failure reason parameter to the telemetry contract so
callers can record diagnostic codes: modify the ITelemetrySnapshotService
interface method RecordAction to accept an additional optional string?
reasonCode = null (i.e., RecordAction(string actionId, AddressSource source,
bool succeeded, string? reasonCode = null)), and update all implementing
classes/methods that implement RecordAction and any internal places that
construct telemetry entries or snapshots (e.g., snapshot creation logic that
reads recorded actions) to capture and store the reasonCode so it is surfaced in
TelemetrySnapshot and ExportSnapshotAsync outputs.
src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs (2)

206-226: Hardcoded blocklist in InferPathHints — consider making it configurable or a static field.

The inline string literal list on Line 215 ("steamapps", "workshop", "content", etc.) is easy to miss during maintenance. Moving it to a private static readonly HashSet<string> would improve readability and make additions more discoverable. Also, as more mods are onboarded, the blocklist may need to grow — an externalized configuration could help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs` around lines 206
- 226, The hardcoded blocklist in InferPathHints should be extracted from the
inline token check into a single reusable static field or configurable option:
create a private static readonly HashSet<string> (e.g., s_pathHintBlocklist) on
the ModOnboardingService class and replace the inline pattern (the token is
"steamapps" or "workshop" or ...) with a membership check like
s_pathHintBlocklist.Contains(token); optionally make it configurable by wiring
the set from options or configuration (e.g., IOptions or constructor parameter)
so the list can be extended without code changes and update any unit tests that
reference InferPathHints accordingly.

113-150: Duplicated normalization logic across NormalizeNamespace, NormalizeProfileId, InferAliases, and TokenizeHintInput.

The pattern of .Trim().ToLowerInvariant().Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray() followed by .Trim('_') appears in at least four places (Lines 120–125, 132–137, 262–267, 283–288). Extracting a shared SanitizeIdentifier(string input) helper would reduce duplication and ensure consistent normalization rules.

♻️ Proposed helper extraction
+    private static string SanitizeIdentifier(string input)
+    {
+        var sanitized = new string(input
+            .Trim()
+            .ToLowerInvariant()
+            .Select(ch => char.IsLetterOrDigit(ch) ? ch : '_')
+            .ToArray())
+            .Trim('_');
+        return sanitized;
+    }
+
     private static string NormalizeNamespace(string? namespaceRoot)
     {
         if (string.IsNullOrWhiteSpace(namespaceRoot))
         {
             return "custom";
         }
 
-        var sanitized = new string(namespaceRoot
-            .Trim()
-            .ToLowerInvariant()
-            .Select(ch => char.IsLetterOrDigit(ch) ? ch : '_')
-            .ToArray())
-            .Trim('_');
-
+        var sanitized = SanitizeIdentifier(namespaceRoot);
         return string.IsNullOrWhiteSpace(sanitized) ? "custom" : sanitized;
     }

Apply the same pattern to NormalizeProfileId and InferAliases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Profiles/Services/ModOnboardingService.cs` around lines 113
- 150, The normalization logic is duplicated across NormalizeNamespace,
NormalizeProfileId, InferAliases, and TokenizeHintInput; extract a single helper
method (e.g., SanitizeIdentifier(string? input)) that performs the shared steps:
null/whitespace handling, Trim(), ToLowerInvariant(), replace non-alphanumeric
chars with '_', then Trim('_') and return the sanitized string; then update
NormalizeNamespace to call SanitizeIdentifier and return "custom" when the
helper yields null/empty, update NormalizeProfileId to call SanitizeIdentifier,
keep the existing InvalidDataException if sanitized is empty, and preserve the
logic that ensures the returned id starts with "custom_" (adding it only if not
present), and replace the same inline logic in InferAliases and
TokenizeHintInput to call SanitizeIdentifier so all normalization is centralized
and consistent.
tests/SwfocTrainer.Tests/Core/SupportBundleServiceTests.cs (1)

49-56: Consider validating manifest content, not just file existence.

The test asserts that the bundle and manifest files exist and that the ZIP contains expected entry names, but doesn't validate the manifest JSON structure (e.g., schemaVersion, includedFiles entries, profileId). Given the manifest consistency bug identified in SupportBundleService.cs, a content assertion here would have caught it. Adding a quick deserialization + field check would strengthen this test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/SwfocTrainer.Tests/Core/SupportBundleServiceTests.cs` around lines 49 -
56, Extend the test to open and deserialize the manifest file at
result.ManifestPath and assert its important fields (e.g., schemaVersion,
includedFiles array contents, and profileId) rather than only checking file
existence; for example, read File.ReadAllText(result.ManifestPath), parse with
System.Text.Json/JsonDocument or Newtonsoft.Json into a dynamic/typed object,
then assert schemaVersion equals the expected value, includedFiles contains the
expected entries (like "runtime-snapshot.json" / telemetry / runs), and
profileId is present and matches the profile used when creating the bundle (this
will surface the manifest consistency bug in SupportBundleService.cs).
src/SwfocTrainer.Core/Services/TelemetrySnapshotService.cs (1)

44-73: UnresolvedRate semantics may be surprising — consider a doc comment.

Line 57 defines "unresolved" as actions that appear in failures but never in successes. This means an action that succeeded once and then failed 100 times is considered "resolved" (not counted in UnresolvedRate). This is a valid definition but may surprise consumers expecting it to reflect recent/current state. A brief doc comment on the TelemetrySnapshot.UnresolvedRate property explaining this would help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Services/TelemetrySnapshotService.cs` around lines 44 -
73, The UnresolvedRate calculation in CreateSnapshot
(unresolvedCount/unresolvedRate) treats an action as "unresolved" only if it
appears in failure counters but never in success counters; update the
TelemetrySnapshot.UnresolvedRate property with a brief XML doc comment that
states this exact semantics (i.e., only-actions seen solely in failures are
counted, so an action that has any success entries is excluded even if it has
many failures), and optionally note that this is a lifetime-snapshot measure
rather than a recent/current-state indicator so consumers are not surprised;
place the comment on the TelemetrySnapshot.UnresolvedRate property to make the
behavior clear.
src/SwfocTrainer.Core/Services/ModCalibrationService.cs (2)

220-227: SanitizeFileToken can return an empty string, producing a malformed artifact filename.

A ProfileId consisting entirely of special characters (e.g., "---") sanitizes to "", yielding calibration--{timestamp}.json. Add a fallback:

🛠️ Proposed fix
 private static string SanitizeFileToken(string value)
 {
-    return new string(value
+    var result = new string(value
         .ToLowerInvariant()
         .Select(ch => char.IsLetterOrDigit(ch) ? ch : '_')
         .ToArray())
         .Trim('_');
+    return string.IsNullOrEmpty(result) ? "unknown" : result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Services/ModCalibrationService.cs` around lines 220 -
227, SanitizeFileToken can return an empty string (e.g., ProfileId of "---"),
producing malformed filenames; update the SanitizeFileToken method so after
sanitizing it checks for empty or all-underscores and returns a safe fallback
(e.g., "unknown" or a deterministic fallback based on the original input)
instead of "", ensuring callers that build filenames (like those that produce
"calibration-{token}-{timestamp}.json") always get a non-empty token; implement
this check and return the fallback string when the sanitized result is empty.

103-109: cancellationToken is accepted but never observed.

Since the method body is entirely synchronous today the token is unused. Either suppress the parameter or document the intent:

🛠️ Proposed fix
 public Task<ModCompatibilityReport> BuildCompatibilityReportAsync(
     TrainerProfile profile,
     AttachSession? session,
     DependencyValidationResult? dependencyValidation = null,
     IReadOnlyDictionary<string, IReadOnlyList<string>>? catalog = null,
-    CancellationToken cancellationToken = default)
+    CancellationToken cancellationToken = default)  // reserved for future async evaluation

Alternatively, add _ = cancellationToken; to make the omission explicit until the method gains real async work.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.Core/Services/ModCalibrationService.cs` around lines 103 -
109, The BuildCompatibilityReportAsync method accepts a CancellationToken
parameter that is never observed; update the method to either explicitly
acknowledge the unused token (e.g., add a single line like "_ =
cancellationToken;" at the top of BuildCompatibilityReportAsync) or remove the
parameter if cancellation is not intended, or document why it is currently
unused—choose one approach and apply it to the BuildCompatibilityReportAsync
signature/body so the unused parameter is no longer silent.

Comment on lines +96 to +108
$args = @(
"release", "create", $tag, $zipPath, $checksumPath,
"--title", "SWFOC Trainer $tag",
"--notes-file", $notesPath,
"--verify-tag"
)

$manualPrerelease = "${{ inputs.prerelease }}" -eq "true"
if ($manualPrerelease -and "${{ github.ref_type }}" -ne "tag") {
$args += "--prerelease"
}

gh @args
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

$args is a PowerShell automatic variable — rename to avoid shadowing.

$args is a reserved automatic variable in PowerShell. Assigning to it shadows the built-in and can produce subtle, hard-to-diagnose issues. Rename it (e.g., $releaseArgs).

🐛 Proposed fix
-          $args = @(
+          $releaseArgs = @(
             "release", "create", $tag, $zipPath, $checksumPath,
             "--title", "SWFOC Trainer $tag",
             "--notes-file", $notesPath,
             "--verify-tag"
           )

           $manualPrerelease = "${{ inputs.prerelease }}" -eq "true"
           if ($manualPrerelease -and "${{ github.ref_type }}" -ne "tag") {
-            $args += "--prerelease"
+            $releaseArgs += "--prerelease"
           }

-          gh `@args`
+          gh `@releaseArgs`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$args = @(
"release", "create", $tag, $zipPath, $checksumPath,
"--title", "SWFOC Trainer $tag",
"--notes-file", $notesPath,
"--verify-tag"
)
$manualPrerelease = "${{ inputs.prerelease }}" -eq "true"
if ($manualPrerelease -and "${{ github.ref_type }}" -ne "tag") {
$args += "--prerelease"
}
gh @args
$releaseArgs = @(
"release", "create", $tag, $zipPath, $checksumPath,
"--title", "SWFOC Trainer $tag",
"--notes-file", $notesPath,
"--verify-tag"
)
$manualPrerelease = "${{ inputs.prerelease }}" -eq "true"
if ($manualPrerelease -and "${{ github.ref_type }}" -ne "tag") {
$releaseArgs += "--prerelease"
}
gh `@releaseArgs`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-portable.yml around lines 96 - 108, The script is
shadowing PowerShell's automatic $args variable; rename the array variable (for
example to $releaseArgs) and update all references: change the declaration that
builds "release", "create", $tag, $zipPath, $checksumPath, "--title",
"--notes-file", "--verify-tag" to the new name, replace the $args +=
"--prerelease" usage (and any other $args manipulations) with $releaseArgs, and
invoke the GitHub CLI with gh `@releaseArgs` instead of gh `@args` so the built-in
automatic variable is no longer overwritten.


## Calibration Artifacts
Artifacts are written to:
- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double backslash renders literally in Markdown inline code spans.

In a backtick inline code span, backslashes are literal — \\ renders as \\, not \. The path will display with double backslashes in the rendered runbook.

📝 Proposed fix
-- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)
+- `%LOCALAPPDATA%\SwfocTrainer\support\calibration` (default app path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)
- `%LOCALAPPDATA%\SwfocTrainer\support\calibration` (default app path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/MOD_ONBOARDING_RUNBOOK.md` at line 27, The inline code span currently
contains `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` which renders with
doubled backslashes; update the runbook so the displayed path uses single
backslashes by replacing that inline code with
`%LOCALAPPDATA%\SwfocTrainer\support\calibration` or convert the path into a
fenced code block (triple backticks) containing
%LOCALAPPDATA%\SwfocTrainer\support\calibration to ensure it renders with single
backslashes; locate the literal
`%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` string in the document and
make the replacement.

Comment on lines +1221 to +1223
var launchLines = OnboardingLaunchSample
.Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.ToArray();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Environment.NewLine-only split drops \n-terminated input (e.g., clipboard pastes from terminal/VS Code).

WPF's TextBox emits \r\n for typed newlines, but pasted text often uses bare \n. With Environment.NewLine as the sole separator, a multi-line paste would be treated as one launch sample.

🛠️ Proposed fix
-var launchLines = OnboardingLaunchSample
-    .Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
-    .ToArray();
+var launchLines = OnboardingLaunchSample
+    .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
+    .ToArray();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var launchLines = OnboardingLaunchSample
.Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.ToArray();
var launchLines = OnboardingLaunchSample
.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.ToArray();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.App/ViewModels/MainViewModel.cs` around lines 1221 - 1223,
The split using Environment.NewLine on OnboardingLaunchSample (producing
launchLines) misses inputs that use bare '\n' (common when pasting), so change
the Split call to use explicit line separators like new[] { "\r\n", "\n", "\r" }
with StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries so
both CRLF and LF-only (and stray CR) line endings are handled and each pasted
line becomes a separate launchLines entry.

Comment on lines +1265 to +1281
private async Task BuildCompatibilityReportAsync()
{
var profileId = SelectedProfileId ?? OnboardingDraftProfileId;
var profile = await _profiles.ResolveInheritedProfileAsync(profileId);
var report = await _modCalibration.BuildCompatibilityReportAsync(
profile,
_runtime.CurrentSession);

ModCompatibilityRows.Clear();
foreach (var action in report.Actions)
{
ModCompatibilityRows.Add($"{action.ActionId} | {action.State} | {action.ReasonCode} | {action.Confidence:0.00}");
}

ModCompatibilitySummary = $"promotionReady={report.PromotionReady} dependency={report.DependencyStatus} unresolvedCritical={report.UnresolvedCriticalSymbols}";
Status = $"Compatibility report generated for {profileId}";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Compatibility report omits catalog data, producing reliability states inconsistent with the reliability panel.

RefreshActionReliabilityAsync (line 1742) loads the catalog before scoring, but BuildCompatibilityReportAsync calls the service without it. Since _actionReliability.Evaluate uses the catalog to score catalog-gated actions, omitting it can flip action states between Healthy and Unavailable, leading operators to an incorrect PromotionReady verdict.

🛠️ Proposed fix
 private async Task BuildCompatibilityReportAsync()
 {
     var profileId = SelectedProfileId ?? OnboardingDraftProfileId;
     var profile = await _profiles.ResolveInheritedProfileAsync(profileId);
+
+    IReadOnlyDictionary<string, IReadOnlyList<string>>? catalog = null;
+    try
+    {
+        catalog = await _catalog.LoadCatalogAsync(profileId);
+    }
+    catch
+    {
+        // Catalog is optional for reliability scoring.
+    }
+
     var report = await _modCalibration.BuildCompatibilityReportAsync(
         profile,
-        _runtime.CurrentSession);
+        _runtime.CurrentSession,
+        catalog: catalog);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async Task BuildCompatibilityReportAsync()
{
var profileId = SelectedProfileId ?? OnboardingDraftProfileId;
var profile = await _profiles.ResolveInheritedProfileAsync(profileId);
var report = await _modCalibration.BuildCompatibilityReportAsync(
profile,
_runtime.CurrentSession);
ModCompatibilityRows.Clear();
foreach (var action in report.Actions)
{
ModCompatibilityRows.Add($"{action.ActionId} | {action.State} | {action.ReasonCode} | {action.Confidence:0.00}");
}
ModCompatibilitySummary = $"promotionReady={report.PromotionReady} dependency={report.DependencyStatus} unresolvedCritical={report.UnresolvedCriticalSymbols}";
Status = $"Compatibility report generated for {profileId}";
}
private async Task BuildCompatibilityReportAsync()
{
var profileId = SelectedProfileId ?? OnboardingDraftProfileId;
var profile = await _profiles.ResolveInheritedProfileAsync(profileId);
IReadOnlyDictionary<string, IReadOnlyList<string>>? catalog = null;
try
{
catalog = await _catalog.LoadCatalogAsync(profileId);
}
catch
{
// Catalog is optional for reliability scoring.
}
var report = await _modCalibration.BuildCompatibilityReportAsync(
profile,
_runtime.CurrentSession,
catalog: catalog);
ModCompatibilityRows.Clear();
foreach (var action in report.Actions)
{
ModCompatibilityRows.Add($"{action.ActionId} | {action.State} | {action.ReasonCode} | {action.Confidence:0.00}");
}
ModCompatibilitySummary = $"promotionReady={report.PromotionReady} dependency={report.DependencyStatus} unresolvedCritical={report.UnresolvedCriticalSymbols}";
Status = $"Compatibility report generated for {profileId}";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.App/ViewModels/MainViewModel.cs` around lines 1265 - 1281,
BuildCompatibilityReportAsync calls
_modCalibration.BuildCompatibilityReportAsync without first loading the catalog,
so catalog-gated actions are scored inconsistently versus
RefreshActionReliabilityAsync which loads the catalog; before calling
_modCalibration.BuildCompatibilityReportAsync in BuildCompatibilityReportAsync,
load the catalog into the action reliability component (the same call/sequence
used in RefreshActionReliabilityAsync, e.g. await
_actionReliability.LoadCatalogAsync(...) using _runtime.CurrentSession/catalog)
so that _actionReliability.Evaluate has the catalog available and
report.PromotionReady and action states match the reliability panel.

Comment on lines +1296 to 1303
private async Task ExportTelemetrySnapshotAsync()
{
var telemetryDir = Path.Combine(SupportBundleOutputDirectory, "telemetry");
Directory.CreateDirectory(telemetryDir);
var path = await _telemetry.ExportSnapshotAsync(telemetryDir);
OpsArtifactSummary = path;
Status = $"Telemetry snapshot exported: {path}";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ExportTelemetrySnapshotAsync always reports success — inconsistent with ExportSupportBundleAsync.

ExportSupportBundleAsync guards on result.Succeeded; this method blindly formats path into a success message even when the path is null/empty. Add a failure check consistent with the bundle export pattern:

🛠️ Proposed fix
 private async Task ExportTelemetrySnapshotAsync()
 {
     var telemetryDir = Path.Combine(SupportBundleOutputDirectory, "telemetry");
     Directory.CreateDirectory(telemetryDir);
     var path = await _telemetry.ExportSnapshotAsync(telemetryDir);
     OpsArtifactSummary = path;
-    Status = $"Telemetry snapshot exported: {path}";
+    Status = !string.IsNullOrWhiteSpace(path)
+        ? $"Telemetry snapshot exported: {path}"
+        : "Telemetry snapshot export failed.";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async Task ExportTelemetrySnapshotAsync()
{
var telemetryDir = Path.Combine(SupportBundleOutputDirectory, "telemetry");
Directory.CreateDirectory(telemetryDir);
var path = await _telemetry.ExportSnapshotAsync(telemetryDir);
OpsArtifactSummary = path;
Status = $"Telemetry snapshot exported: {path}";
}
private async Task ExportTelemetrySnapshotAsync()
{
var telemetryDir = Path.Combine(SupportBundleOutputDirectory, "telemetry");
Directory.CreateDirectory(telemetryDir);
var path = await _telemetry.ExportSnapshotAsync(telemetryDir);
OpsArtifactSummary = path;
Status = !string.IsNullOrWhiteSpace(path)
? $"Telemetry snapshot exported: {path}"
: "Telemetry snapshot export failed.";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SwfocTrainer.App/ViewModels/MainViewModel.cs` around lines 1296 - 1303,
ExportTelemetrySnapshotAsync currently always sets OpsArtifactSummary and a
success Status even when ExportSnapshotAsync fails/returns null or empty; change
it to mirror the ExportSupportBundleAsync pattern by checking the returned path
(e.g., if string.IsNullOrEmpty(path) or a returned result.Succeeded equivalent).
If the export failed, set Status to a clear failure message and avoid assigning
OpsArtifactSummary (or clear it), otherwise assign OpsArtifactSummary = path and
set the success Status. Use the existing method name
ExportTelemetrySnapshotAsync and the variables path, OpsArtifactSummary and
Status to locate and implement the conditional handling.

[PSCustomObject]@{
FullPath = $_.FullName
RelativePath = $_.FullName.Substring((Resolve-Path $Root).Path.Length).TrimStart('\\', '/')
RelativePath = ($_.FullName.Substring((Resolve-Path $Root).Path.Length) -replace '^[\\/]+', '')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for test files, fixtures, or CI references to compare-visual-pack
echo "=== Searching for compare-visual-pack references outside the script itself ==="
rg -i "compare-visual-pack" --type yaml --type powershell -g "!tools/compare-visual-pack.ps1" 2>/dev/null || echo "No matches in yaml/ps1"

echo -e "\n=== Checking GitHub workflows ==="
find .github -type f \( -name "*.yml" -o -name "*.yaml" \) -exec grep -l "compare-visual-pack" {} \; 2>/dev/null || echo "No matches in workflows"

echo -e "\n=== Checking test directories ==="
find tests -type f \( -name "*.ps1" -o -name "*.js" -o -name "*.ts" \) -exec grep -l "compare-visual-pack" {} \; 2>/dev/null || echo "No matches in tests"

echo -e "\n=== Listing all PowerShell test files ==="
find tests -type f -name "*.ps1" 2>/dev/null | head -20 || echo "No test.ps1 files found"

echo -e "\n=== Checking tools directory structure ==="
ls -la tools/ 2>/dev/null | head -20

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 1745


🏁 Script executed:

#!/bin/bash
# Verify the content and structure of compare-visual-pack.ps1
echo "=== compare-visual-pack.ps1 context (lines 20-40) ==="
sed -n '20,40p' tools/compare-visual-pack.ps1

echo -e "\n=== Looking for test invocations or fixtures ==="
find . -type f \( -name "*.md" -o -name "*.ps1" -o -name "*.yml" \) -exec grep -l "compare.visual" {} \; 2>/dev/null || echo "No matches"

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 909


🏁 Script executed:

#!/bin/bash
# Check if there's a test script or smoke test framework in the repo
echo "=== Looking for test runners or fixture frameworks ==="
find . -path ./node_modules -prune -o -type f \( -name "*test*.ps1" -o -name "*smoke*.ps1" -o -name "*fixture*.ps1" \) -print 2>/dev/null | head -20

echo -e "\n=== Checking for CI test runners ==="
find .github -type f -name "*.yml" -o -name "*.yaml" | xargs grep -l "test\|Test" 2>/dev/null | head -5

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Inspect the visual-audit.yml workflow to understand how compare-visual-pack.ps1 is invoked
echo "=== visual-audit.yml workflow content ==="
cat .github/workflows/visual-audit.yml

echo -e "\n=== Checking VISUAL_AUDIT_RUNBOOK.md for details ==="
head -60 ./docs/VISUAL_AUDIT_RUNBOOK.md

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 3497


🏁 Script executed:

#!/bin/bash
# Check if this is a new script or an existing one in the git history
echo "=== Git history of compare-visual-pack.ps1 ==="
git log --oneline -n 20 -- tools/compare-visual-pack.ps1 2>/dev/null || echo "No git history available"

echo -e "\n=== Checking compare-visual-pack.ps1 for any inline test/validation logic ==="
cat tools/compare-visual-pack.ps1 | grep -i "test\|fixture\|validation\|example" || echo "No inline test comments found"

echo -e "\n=== Full compare-visual-pack.ps1 to understand context ==="
cat tools/compare-visual-pack.ps1

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 3821


🏁 Script executed:

#!/bin/bash
# Verify if tools/AGENTS.md or any documentation specifies what "deterministic fixture or smoke command" means for this project
echo "=== Checking tools/AGENTS.md for guidance definition ==="
cat tools/AGENTS.md

echo -e "\n=== Checking if there's a fixtures directory structure in tools/ ==="
ls -la tools/fixtures/ 2>/dev/null || echo "No fixtures/ directory in tools/"

echo -e "\n=== Checking if any tools have documented smoke/test invocations ==="
head -30 tools/*.ps1 | grep -E "^(#|param|\.SYNOPSIS|\.EXAMPLE)" | head -30

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 1767


🏁 Script executed:

#!/bin/bash
# Check what's in the visual fixtures directory
echo "=== Contents of tools/fixtures/visual/ ==="
ls -la tools/fixtures/visual/

echo -e "\n=== Checking for any compare-visual-pack test or fixture references ==="
find tools/fixtures -type f -exec grep -l "compare" {} \;

echo -e "\n=== Checking other tools for how they document smoke commands ==="
grep -r "Example\|example\|smoke\|test" tools/*.ps1 2>/dev/null | head -20

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 2028


Add deterministic fixture or smoke command for compare-visual-pack.ps1 script changes.

Per project coding guidelines ("Every tooling script change must include a deterministic fixture or smoke command"), this new script requires a standalone test fixture or documented invocation command. While visual-audit.yml provides CI integration, this does not fulfill the requirement for a portable, reproducible fixture that developers can execute locally.

Add a fixture under tools/fixtures/visual/ with sample baseline and candidate images, or document an explicit smoke command showing how to verify the script's behavior independently (e.g., instructions in a comment or separate fixture invocation script).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/compare-visual-pack.ps1` at line 28, Add a deterministic smoke fixture
or invocation for the new tools/compare-visual-pack.ps1 script: create a
tools/fixtures/visual/ directory containing a minimal, checked-in baseline and
candidate image pair plus a README or small script that demonstrates the exact
PowerShell invocation (e.g., calling compare-visual-pack.ps1 with those files
and expected exit code), or alternatively add a clear commented "smoke command"
example at the top of compare-visual-pack.ps1 showing the concrete invocation
and expected output; ensure the fixture names and invocation match the script's
parameter names so developers can run the test locally without CI (reference:
compare-visual-pack.ps1 and visual-audit.yml).

Comment on lines +18 to +21
"generatedAtUtc": {
"type": "string",
"format": "date-time"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

JSON Schema 2020-12 format annotation vs format assertion validation default behavior

💡 Result:

In JSON Schema 2020-12, format was split into two vocabularies with different semantics:

  • format-annotation: format is annotation-only (it communicates intent/semantics but does not make validation fail). This vocabulary is required by the default 2020-12 meta-schema, so this is the default behavior. (json-schema.org)

    • Implementations may offer an option to also validate formats here, but that option must be disabled by default (i.e., default = do not assert on format). (json-schema.org)
  • format-assertion: format is a true assertion keyword (invalid formats must fail validation). This vocabulary is optional in the spec, and when a schema/dialect declares it as required (via $vocabulary: { …: true }), an implementation must perform format validation for the defined formats (or refuse to process the schema if it can’t). (json-schema.org)

So, by default in 2020-12, format should not cause validation failures unless you (a) use a dialect/meta-schema that enables format-assertion, or (b) explicitly turn on an implementation-specific “validate formats” option (which must default to off). (json-schema.org)

Citations:


🌐 Web query:

NJsonSchema JsonSchema.Net format "date-time" validation default 2020-12

💡 Result:

In both NJsonSchema and JsonSchema.Net, "format": "date-time" is intended to validate an RFC 3339 date-time string (i.e., a full date and time, with a T separator and a timezone like Z or an offset). "2020-12" is just year-month, so it will fail "date-time" validation. Valid examples look like 2002-10-02T15:00:00Z. [1], [2]

If you want to keep a value like "2020-12":

  • It is not a "date-time" (or even a "date"; "date" expects YYYY-MM-DD). [1]
  • Use a custom constraint instead, e.g. a regex: "pattern": "^\\d{4}-\\d{2}$", or register a custom format validator (library-specific). [3]

Also: “2020-12” is commonly seen as the JSON Schema draft name (Draft 2020-12), which is unrelated to the "date-time" string format. [4]

Sources
[1] learnjsonschema.com – format (2020-12) / date-time listed under 2020-12 validation
[2] W3C validator docs – RFC 3339 date-time explanation + valid examples
[3] JsonSchema.Net docs – format validation is configurable and supports date-time
[4] JsonSchema.Net docs – supported drafts include Draft 2020-12


🌐 Web query:

JsonSchema.Net .NET PowerShell format validation does it enforce date-time

💡 Result:

JsonSchema.Net can enforce "format": "date-time" (it has a built-in date-time format validator), but it does not necessarily enforce it by default because format is treated as an annotation unless you opt into “format-as-assertion” behavior. (docs.json-everything.net)

How to make it enforce date-time

Enable format validation in evaluation options:

  • Current docs: set EvaluationOptions.RequireFormatValidation = true when calling JsonSchema.Evaluate(). (docs.json-everything.net)
  • Release note (older naming / change): a library option named ValidationOptions.ValidateFormat was added so you can configure whether format is validated, and the default behavior was aligned so format is not validated by default. (docs.json-everything.net)

So, in PowerShell/.NET: if you’re just evaluating a schema without enabling that option, "format": "date-time" may not be enforced; once enabled, it will validate (using the library’s built-in date-time checker). (docs.json-everything.net)

Citations:


🏁 Script executed:

cat tools/schemas/support-bundle-manifest.schema.json | head -30

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 766


format: "date-time" does not enforce validation under JSON Schema 2020-12 without explicit opt-in.

In JSON Schema 2020-12, the "format-annotation" vocabulary — used in the default meta-schema — treats format as an annotation only, not an assertion. By default, this keyword does not perform validation. JsonSchema.Net, commonly used in .NET/PowerShell tooling, aligns with this specification: it requires explicit configuration (e.g., EvaluationOptions.RequireFormatValidation = true) to validate format constraints, and this validation is disabled by default.

Without a pattern constraint or explicit validator enablement, an invalid string such as "not-a-date" will pass schema validation against this field. To ensure robust, interoperable validation across tools and implementations, combine format with a pattern constraint:

🛡️ Proposed fix
     "generatedAtUtc": {
       "type": "string",
-      "format": "date-time"
+      "format": "date-time",
+      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"generatedAtUtc": {
"type": "string",
"format": "date-time"
},
"generatedAtUtc": {
"type": "string",
"format": "date-time",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/schemas/support-bundle-manifest.schema.json` around lines 18 - 21, The
"generatedAtUtc" property currently uses "format": "date-time" which is only an
annotation under JSON Schema 2020-12 and won't enforce validation by default;
update the "generatedAtUtc" schema (the generatedAtUtc object) to include a
validating "pattern" regex that enforces an RFC3339/ISO8601 datetime (e.g. a
regex covering YYYY-MM-DDTHH:MM:SS with optional fractional seconds and a Z or
±HH:MM offset) so tools that don't enable format validation still reject invalid
strings; keep "type": "string" and retain "format": "date-time" as documentation
while adding the pattern constraint to ensure interoperability.

Comment on lines 68 to 78
for ($i = 0; $i -lt $candidates.Count; $i++) {
$candidate = $candidates[$i]
foreach ($required in @("symbol", "source", "healthStatus", "confidence")) {
Require-Field -Object $candidate -Field $required
}

$confidence = [double]$candidate.confidence
if ($confidence -lt 0 -or $confidence -gt 1) {
Add-Error "candidates[$i].confidence must be between 0 and 1"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe cast [double]$candidate.confidence can throw an unhandled exception.

If confidence is a non-numeric value (e.g., "high" or $null), the [double] cast on Line 74 will throw a terminating error before reaching the range check. Since $ErrorActionPreference = "Stop", this terminates the entire script with a raw PowerShell error rather than a clean validation message.

🛡️ Proposed defensive cast
-    $confidence = [double]$candidate.confidence
-    if ($confidence -lt 0 -or $confidence -gt 1) {
-        Add-Error "candidates[$i].confidence must be between 0 and 1"
-    }
+    $rawConfidence = $candidate.confidence
+    $confidence = 0.0
+    if ($null -eq $rawConfidence -or -not [double]::TryParse([string]$rawConfidence, [ref]$confidence)) {
+        Add-Error "candidates[$i].confidence must be a number"
+    } elseif ($confidence -lt 0 -or $confidence -gt 1) {
+        Add-Error "candidates[$i].confidence must be between 0 and 1"
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for ($i = 0; $i -lt $candidates.Count; $i++) {
$candidate = $candidates[$i]
foreach ($required in @("symbol", "source", "healthStatus", "confidence")) {
Require-Field -Object $candidate -Field $required
}
$confidence = [double]$candidate.confidence
if ($confidence -lt 0 -or $confidence -gt 1) {
Add-Error "candidates[$i].confidence must be between 0 and 1"
}
}
for ($i = 0; $i -lt $candidates.Count; $i++) {
$candidate = $candidates[$i]
foreach ($required in @("symbol", "source", "healthStatus", "confidence")) {
Require-Field -Object $candidate -Field $required
}
$rawConfidence = $candidate.confidence
$confidence = 0.0
if ($null -eq $rawConfidence -or -not [double]::TryParse([string]$rawConfidence, [ref]$confidence)) {
Add-Error "candidates[$i].confidence must be a number"
} elseif ($confidence -lt 0 -or $confidence -gt 1) {
Add-Error "candidates[$i].confidence must be between 0 and 1"
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` around lines 68 - 78, The cast
[double]$candidate.confidence can throw for non-numeric values; change the logic
in the candidates loop (where Require-Field is called and Add-Error is used) to
defensively attempt conversion (e.g., use the -as operator or TryParse
semantics) so that non-numeric or null confidences produce a controlled
Add-Error message (like "candidates[$i].confidence must be a number between 0
and 1") instead of letting a terminating exception bubble up, then only perform
the range check when the conversion succeeds.

Comment on lines 77 to 86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Strict mode check for "manifest.json" will always fail with current SupportBundleService output.

As noted in the review of SupportBundleService.cs, the manifest's includedFiles is serialized before "manifest.json" is appended to the list. This means the strict-mode check on Line 78 will always report a failure. Once the service-side ordering is fixed (adding "manifest.json" before serialization), this check will work correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-support-bundle-manifest.ps1` around lines 77 - 86, The
strict-mode manifest check currently requires "manifest.json" be present in
$included but SupportBundleService serializes includedFiles before appending
manifest.json, causing a false failure; modify the strict-mode block (the
$Strict conditional that inspects $included and calls Add-Error) to stop
requiring "manifest.json" in $included (remove or disable the Add-Error call
that checks for "manifest.json") and retain the runtime-snapshot check, or
alternatively gate the manifest.json requirement behind a feature flag/boolean
so it only runs once the service-side ordering is fixed.

- Release packages include checksum files for integrity verification.
- Do not consume unsigned/unverified artifacts from non-release channels for production usage.

## Rollback Procedure

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Rollback Procedure
## Rollback Procedure

pwsh ./tools/validate-calibration-artifact.ps1 -ArtifactPath <artifact.json> -SchemaPath tools/schemas/calibration-artifact.schema.json -Strict
```

## Compatibility Report Interpretation

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Compatibility Report Interpretation
## Compatibility Report Interpretation


## Dry-Run Evidence Template
Use this in issue/PR comments:
- Workflow run URL

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- Workflow run URL
- Workflow run URL

- `localPathHints` from `MODPATH=...` and process/path tokens
- `profileAliases` from draft id + display name (+ optional aliases)

## Calibration Artifacts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Calibration Artifacts
## Calibration Artifacts

- `publish_release=false`: package + artifact only (no GitHub Release publish).
- `publish_release=true`: publish/update release for `tag_name`.

## Artifact Verification

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Artifact Verification
## Artifact Verification

## Goal
Generate a draft custom profile, export calibration evidence, and compute a promotion readiness report.

## UI Flow (Profiles & Updates tab)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## UI Flow (Profiles & Updates tab)
## UI Flow (Profiles & Updates tab)

- Runtime actions are profile-gated and may be unavailable when dependency markers are unresolved.
- Some features remain behind reliability gates to prevent unsafe writes.

## Verification

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Verification
## Verification

5. Click `Export Calibration Artifact`.
6. Click `Build Compatibility Report`.

## Draft Profile Output

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Draft Profile Output
## Draft Profile Output

- Some features remain behind reliability gates to prevent unsafe writes.

## Verification
- Validate checksum before use:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- Validate checksum before use:
- Validate checksum before use:

## Highlights
- Summary of key changes in this release.

## Included Artifacts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Included Artifacts
## Included Artifacts

```

This triggers `.github/workflows/release-portable.yml` to:
- build release binaries,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- build release binaries,
- build release binaries,

- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)

Schema contract:
- `tools/schemas/calibration-artifact.schema.json`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `tools/schemas/calibration-artifact.schema.json`
- `tools/schemas/calibration-artifact.schema.json`

The computed hash must match the `.sha256` file.

## Security Controls
- Tags for published releases are immutable by policy.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- Tags for published releases are immutable by policy.
- Tags for published releases are immutable by policy.


### Manual dispatch
Use workflow dispatch for dry runs or controlled publish.
- `publish_release=false`: package + artifact only (no GitHub Release publish).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `publish_release=false`: package + artifact only (no GitHub Release publish).
- `publish_release=false`: package + artifact only (no GitHub Release publish).

- Summary of key changes in this release.

## Included Artifacts
- `SwfocTrainer-portable.zip`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `SwfocTrainer-portable.zip`
- `SwfocTrainer-portable.zip`


## Calibration Artifacts
Artifacts are written to:
- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)
- `%LOCALAPPDATA%\\SwfocTrainer\\support\\calibration` (default app path)


## Compatibility Report Interpretation
Promotion is blocked when any of these are true:
- dependency status is `HardFail`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- dependency status is `HardFail`
- dependency status is `HardFail`

- `profiles/custom/profiles/<draftProfileId>.json` by default.

The scaffold process infers:
- `requiredWorkshopIds` from `STEAMMOD=...`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `requiredWorkshopIds` from `STEAMMOD=...`
- `requiredWorkshopIds` from `STEAMMOD=...`

- `SwfocTrainer-portable.zip.sha256`

## Known Limitations
- Live profile validation still requires local SWFOC sessions on Windows.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- Live profile validation still requires local SWFOC sessions on Windows.
- Live profile validation still requires local SWFOC sessions on Windows.

@@ -0,0 +1,52 @@
# Release Runbook

## Channel Decision

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Channel Decision
## Channel Decision

- Do not consume unsigned/unverified artifacts from non-release channels for production usage.

## Rollback Procedure
1. Identify the bad release tag and the last known-good tag.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
1. Identify the bad release tag and the last known-good tag.
1. Identify the bad release tag and the last known-good tag.


## Draft Profile Output
Draft profile files are emitted under:
- `profiles/custom/profiles/<draftProfileId>.json` by default.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- `profiles/custom/profiles/<draftProfileId>.json` by default.
- `profiles/custom/profiles/<draftProfileId>.json` by default.

- `SwfocTrainer-portable.zip`
- `SwfocTrainer-portable.zip.sha256`

## Known Limitations

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Known Limitations
## Known Limitations

# SWFOC Trainer {{TAG}}

## Highlights
- Summary of key changes in this release.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Lists should be surrounded by blank lines

Suggested change
- Summary of key changes in this release.
- Summary of key changes in this release.

Get-Content .\SwfocTrainer-portable.zip.sha256
```

## Rollback

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy has a fix for the issue: Expected: 1; Actual: 0; Below

Suggested change
## Rollback
## Rollback

@Prekzursil Prekzursil merged commit 442760c into main Feb 18, 2026
14 of 16 checks passed
@Prekzursil Prekzursil deleted the feature/full-issue-closure-m3-m4 branch February 18, 2026 02:59
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c3ac6c0f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +131
: session.Symbols.Symbols
.Where(x => criticalSymbols.Contains(x.Key))
.Count(x => x.Value.HealthStatus == SymbolHealthStatus.Unresolved);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count missing critical symbols as unresolved

In BuildCompatibilityReportAsync, unresolved critical symbols are computed only from symbols that exist in session.Symbols.Symbols, so any symbol listed in profile.Metadata["criticalSymbols"] but absent from the session map is treated as resolved. In that case promotionReady can be set to true even though a required critical symbol was never resolved, which undermines the promotion gate for custom mod profiles.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
tools/validate-calibration-artifact.ps1 (2)

3-3: $SchemaPath default is CWD-relative — same fragility as the other two validator scripts. Replace with Join-Path $PSScriptRoot "schemas/calibration-artifact.schema.json".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` at line 3, The default $SchemaPath
in tools/validate-calibration-artifact.ps1 is CWD-relative and fragile; change
its initialization to build an absolute path using $PSScriptRoot and Join-Path
(e.g., set $SchemaPath by calling Join-Path $PSScriptRoot
"schemas/calibration-artifact.schema.json") so the script reliably locates the
calibration-artifact.schema.json regardless of the current working directory.

23-33: Lines 31–33 duplicate the required-field loop already performed on Lines 23–25.

If generatedAtUtc, profileId, moduleFingerprint, and candidates are all listed in $schema.required (the likely intent), Require-ValidationField is called twice for each, producing duplicate error messages for any missing field. Remove the explicit hardcoded list if the schema already covers it, or add a brief comment explaining why it's intentionally redundant (e.g., as a compile-time schema-drift guard).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` around lines 23 - 33, The second
foreach loop re-invokes Require-ValidationField for the same properties already
iterated from $schema.required, causing duplicate error messages; either remove
the explicit foreach (@("generatedAtUtc", "profileId", "moduleFingerprint",
"candidates")) entirely or, if you intend redundancy as a defensive guard, keep
it but add a concise comment explaining that it is intentionally duplicated to
protect against schema drift; update the code around Require-ValidationField,
$schema.required, and Add-ValidationError accordingly so each required field is
validated exactly once or clearly documented if duplicated.
tools/validation-helpers.ps1 (2)

16-16: Require- is not an approved PowerShell verb — rename to avoid PSScriptAnalyzer noise.

PSScriptAnalyzer flags Require-ValidationField with PSUseApprovedVerbs. While this is a dot-sourced helper (not a module), the warning still surfaces in any PSScriptAnalyzer pass and CI linting. Consider renaming to Assert-ValidationField or Confirm-ValidationField, both of which are approved verbs and semantically accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validation-helpers.ps1` at line 16, The function name
Require-ValidationField uses an unapproved PowerShell verb; rename the function
to an approved verb (e.g., Assert-ValidationField or Confirm-ValidationField)
and update all references: change the function declaration
Require-ValidationField to Assert-ValidationField, update any callers,
dot-sourcing imports, unit tests and documentation to the new name, and
remove/suppress any PSUseApprovedVerbs ignores; run PSScriptAnalyzer to verify
the PSUseApprovedVerbs warning is resolved.

51-60: Replace Write-Host with redirectable output cmdlets.

PSScriptAnalyzer flags all three Write-Host calls (PSAvoidUsingWriteHost). In non-interactive CI hosts, Write-Host output bypasses the standard streams and cannot be captured or redirected cleanly. Per the coding guideline that script outputs must be stable, prefer Write-Error for the failure path (which writes to stream 2 and is visible in CI logs) and Write-Information or Write-Host-equivalent-safe output for the success message.

♻️ Proposed refactor
     if ($Errors.Count -gt 0) {
-        Write-Host "$Label validation failed:" -ForegroundColor Red
+        Write-Error "$Label validation failed:"
         foreach ($err in $Errors) {
-            Write-Host " - $err" -ForegroundColor Red
+            Write-Error " - $err"
         }
         exit 1
     }

-    Write-Host "$Label validation passed: $Path"
+    Write-Information "$Label validation passed: $Path" -InformationAction Continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validation-helpers.ps1` around lines 51 - 60, Replace the three
Write-Host calls so output is redirectable: on failure when $Errors.Count -gt 0,
call Write-Error to emit the overall "$Label validation failed:" message and
write each error with Write-Error (or use Write-Error for the combined message
and include $err details) instead of Write-Host; on success replace the final
Write-Host "$Label validation passed: $Path" with a redirectable stream such as
Write-Output or Write-Information so CI can capture it. Update the blocks that
reference $Errors, $Label, and $Path accordingly and preserve the same message
text and exit 1 behavior for the failure path.
tools/validate-repro-bundle.ps1 (1)

3-3: $SchemaPath default is CWD-relative — same issue as in validate-support-bundle-manifest.ps1. Use Join-Path $PSScriptRoot "schemas/repro-bundle.schema.json" to be consistent with the dot-source pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-repro-bundle.ps1` at line 3, The default $SchemaPath is
currently CWD-relative; change its initialization so it builds an absolute path
using the script directory instead of the working directory — use $PSScriptRoot
combined with the schemas folder (via Join-Path) to point to
"schemas/repro-bundle.schema.json" so the variable $SchemaPath always resolves
correctly when the script is dot-sourced or run from another CWD.
tools/validate-support-bundle-manifest.ps1 (1)

3-3: $SchemaPath default is CWD-relative, not $PSScriptRoot-relative.

"tools/schemas/support-bundle-manifest.schema.json" resolves relative to the current working directory. When the script is invoked from within tools/ (or any directory other than the repo root), Test-Path fails immediately. The dot-source at Line 9 already uses $PSScriptRoot correctly — the default $SchemaPath should follow the same pattern.

♻️ Proposed fix
-    [string]$SchemaPath = "tools/schemas/support-bundle-manifest.schema.json",
+    [string]$SchemaPath = (Join-Path $PSScriptRoot "schemas/support-bundle-manifest.schema.json"),

The same issue exists in validate-repro-bundle.ps1 (Line 3) and validate-calibration-artifact.ps1 (Line 3).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-support-bundle-manifest.ps1` at line 3, The default
$SchemaPath is CWD-relative which breaks Test-Path when the script is run from a
different working directory; change the $SchemaPath default to be
$PSScriptRoot-relative (use Join-Path or string-expand $PSScriptRoot with the
schemas/support-bundle-manifest.schema.json relative path) so it resolves
correctly regardless of CWD, and apply the same fix to the $SchemaPath defaults
in validate-repro-bundle.ps1 and validate-calibration-artifact.ps1; keep the
rest of the logic (including the dot-sourcing that already uses $PSScriptRoot)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/validate-calibration-artifact.ps1`:
- Around line 52-56: Replace the unsafe guard that directly accesses
$artifact.process with a property-existence check using PSObject.Properties so
the code does not reference a missing property; e.g. change the if condition to
check $artifact.PSObject.Properties['process'] (or use
$artifact.PSObject.Properties.Name -contains 'process') and then run the same
foreach calling Require-ValidationField -Object $artifact.process -Field
$required -Errors $errors -Prefix "process".

In `@tools/validate-repro-bundle.ps1`:
- Around line 69-79: The code currently accesses nested properties
($bundle.launchContext, $bundle.runtimeMode, $bundle.diagnostics) directly under
Set-StrictMode which throws PropertyNotFoundException if those keys are missing;
change the calls so you first safely extract each nested object using the
PSObject.Properties/null-check pattern (e.g.,
$bundle.PSObject.Properties['launchContext']?.Value) or test for $null before
passing to Require-ValidationField, and ensure Require-ValidationField is
invoked only when the extracted value is non-null (or pass $null explicitly if
AllowNull is intended) so validation errors are collected instead of the script
terminating; update the call sites that reference Require-ValidationField
accordingly.

In `@tools/validate-support-bundle-manifest.ps1`:
- Around line 42-47: Avoid accessing $manifest.warnings directly when the key
may be missing and prevent @($null) producing a one-element null array: first
check whether the manifest actually contains the "warnings" property and that it
is not $null (e.g. if ($manifest.PSObject.Properties.Name -contains 'warnings'
-and $manifest.warnings -ne $null) { $warnings = @($manifest.warnings) } else {
$warnings = @() }), then iterate $warnings and call Add-ValidationError as
before; this ensures no PropertyNotFoundException under Set-StrictMode and
avoids the false-positive "warnings[0] must not be null".

In `@tools/validation-helpers.ps1`:
- Around line 7-14: All three functions (Add-ValidationError,
Require-ValidationField, Write-ValidationResult) currently accept $Errors
without marking it mandatory; under Set-StrictMode this can yield a null and
cause a NullReferenceException on .Add()/.Count. Update the parameter
declarations for $Errors in each function to include [Parameter(Mandatory =
$true)] while keeping the existing type ([System.Collections.ArrayList]) so
callers get a clear missing-parameter error instead of a runtime null
dereference.

---

Duplicate comments:
In `@tools/validate-calibration-artifact.ps1`:
- Around line 46-49: Replace the unsafe cast of $candidate.confidence with a
TryParse guard: attempt to parse $candidate.confidence into a [double] (using
[double]::TryParse with a [ref] variable for $confidence) and if parsing fails
call Add-ValidationError -Errors $errors -Message "candidates[$i].confidence
must be a numeric value between 0 and 1"; only when TryParse returns true
perform the existing range check (if ($confidence -lt 0 -or $confidence -gt 1)
...). This ensures non-numeric/null values produce a validation error instead of
a terminating cast exception.

In `@tools/validate-support-bundle-manifest.ps1`:
- Around line 50-52: The strict-mode check currently errors if $included does
not contain "manifest.json", but SupportBundleService appends "manifest.json"
after serializing includedFiles so this will always fail; update the validation
in tools/validate-support-bundle-manifest.ps1 to stop requiring "manifest.json"
to be present in $included (either remove the Add-ValidationError call or change
the condition to ignore "manifest.json" by evaluating $includedFiltered =
$included | Where-Object { $_ -ne "manifest.json" } and perform the strict-mode
existence checks against $includedFiltered), leaving the Add-ValidationError
invocation only for genuine missing entries; keep references to the same symbols
($included and Add-ValidationError) so the change is localized.

---

Nitpick comments:
In `@tools/validate-calibration-artifact.ps1`:
- Line 3: The default $SchemaPath in tools/validate-calibration-artifact.ps1 is
CWD-relative and fragile; change its initialization to build an absolute path
using $PSScriptRoot and Join-Path (e.g., set $SchemaPath by calling Join-Path
$PSScriptRoot "schemas/calibration-artifact.schema.json") so the script reliably
locates the calibration-artifact.schema.json regardless of the current working
directory.
- Around line 23-33: The second foreach loop re-invokes Require-ValidationField
for the same properties already iterated from $schema.required, causing
duplicate error messages; either remove the explicit foreach
(@("generatedAtUtc", "profileId", "moduleFingerprint", "candidates")) entirely
or, if you intend redundancy as a defensive guard, keep it but add a concise
comment explaining that it is intentionally duplicated to protect against schema
drift; update the code around Require-ValidationField, $schema.required, and
Add-ValidationError accordingly so each required field is validated exactly once
or clearly documented if duplicated.

In `@tools/validate-repro-bundle.ps1`:
- Line 3: The default $SchemaPath is currently CWD-relative; change its
initialization so it builds an absolute path using the script directory instead
of the working directory — use $PSScriptRoot combined with the schemas folder
(via Join-Path) to point to "schemas/repro-bundle.schema.json" so the variable
$SchemaPath always resolves correctly when the script is dot-sourced or run from
another CWD.

In `@tools/validate-support-bundle-manifest.ps1`:
- Line 3: The default $SchemaPath is CWD-relative which breaks Test-Path when
the script is run from a different working directory; change the $SchemaPath
default to be $PSScriptRoot-relative (use Join-Path or string-expand
$PSScriptRoot with the schemas/support-bundle-manifest.schema.json relative
path) so it resolves correctly regardless of CWD, and apply the same fix to the
$SchemaPath defaults in validate-repro-bundle.ps1 and
validate-calibration-artifact.ps1; keep the rest of the logic (including the
dot-sourcing that already uses $PSScriptRoot) unchanged.

In `@tools/validation-helpers.ps1`:
- Line 16: The function name Require-ValidationField uses an unapproved
PowerShell verb; rename the function to an approved verb (e.g.,
Assert-ValidationField or Confirm-ValidationField) and update all references:
change the function declaration Require-ValidationField to
Assert-ValidationField, update any callers, dot-sourcing imports, unit tests and
documentation to the new name, and remove/suppress any PSUseApprovedVerbs
ignores; run PSScriptAnalyzer to verify the PSUseApprovedVerbs warning is
resolved.
- Around line 51-60: Replace the three Write-Host calls so output is
redirectable: on failure when $Errors.Count -gt 0, call Write-Error to emit the
overall "$Label validation failed:" message and write each error with
Write-Error (or use Write-Error for the combined message and include $err
details) instead of Write-Host; on success replace the final Write-Host "$Label
validation passed: $Path" with a redirectable stream such as Write-Output or
Write-Information so CI can capture it. Update the blocks that reference
$Errors, $Label, and $Path accordingly and preserve the same message text and
exit 1 behavior for the failure path.

Comment on lines +52 to +56
if ($null -ne $artifact.process) {
foreach ($required in @("pid", "name", "path", "commandLineAvailable", "launchKind", "launchReasonCode")) {
Require-ValidationField -Object $artifact.process -Field $required -Errors $errors -Prefix "process"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

$null -ne $artifact.process guard itself throws under strict mode when process is absent.

The intent of Line 52 is to make process optional, but the guard expression $artifact.process directly accesses the property. With version 2.0 strict mode, PowerShell returns an error if you reference uninitialized variables or non-existent properties. If process is not present as a JSON key at all, this line throws PropertyNotFoundException before the guard comparison is evaluated. Use the safe PSObject.Properties intrinsic for the check:

🛡️ Proposed fix
-if ($null -ne $artifact.process) {
-    foreach ($required in @("pid", "name", "path", "commandLineAvailable", "launchKind", "launchReasonCode")) {
-        Require-ValidationField -Object $artifact.process -Field $required -Errors $errors -Prefix "process"
-    }
-}
+$processProp = $artifact.PSObject.Properties['process']
+if ($null -ne $processProp -and $null -ne $processProp.Value) {
+    foreach ($required in @("pid", "name", "path", "commandLineAvailable", "launchKind", "launchReasonCode")) {
+        Require-ValidationField -Object $processProp.Value -Field $required -Errors $errors -Prefix "process"
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($null -ne $artifact.process) {
foreach ($required in @("pid", "name", "path", "commandLineAvailable", "launchKind", "launchReasonCode")) {
Require-ValidationField -Object $artifact.process -Field $required -Errors $errors -Prefix "process"
}
}
$processProp = $artifact.PSObject.Properties['process']
if ($null -ne $processProp -and $null -ne $processProp.Value) {
foreach ($required in @("pid", "name", "path", "commandLineAvailable", "launchKind", "launchReasonCode")) {
Require-ValidationField -Object $processProp.Value -Field $required -Errors $errors -Prefix "process"
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-calibration-artifact.ps1` around lines 52 - 56, Replace the
unsafe guard that directly accesses $artifact.process with a property-existence
check using PSObject.Properties so the code does not reference a missing
property; e.g. change the if condition to check
$artifact.PSObject.Properties['process'] (or use
$artifact.PSObject.Properties.Name -contains 'process') and then run the same
foreach calling Require-ValidationField -Object $artifact.process -Field
$required -Errors $errors -Prefix "process".

Comment on lines 69 to 79
foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
Require-Field -Object $bundle.launchContext -Field $required -AllowNull
Require-ValidationField -Object $bundle.launchContext -Field $required -Errors $errors -AllowNull
}

foreach ($required in @("hint", "effective", "reasonCode")) {
Require-Field -Object $bundle.runtimeMode -Field $required
Require-ValidationField -Object $bundle.runtimeMode -Field $required -Errors $errors
}

foreach ($required in @("dependencyState", "helperReadiness", "symbolHealthSummary")) {
Require-Field -Object $bundle.diagnostics -Field $required
Require-ValidationField -Object $bundle.diagnostics -Field $required -Errors $errors
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check PowerShell version targets in CI workflows
rg -n "pwsh|powershell" .github/workflows/ -A 2 -B 2

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 5685


🏁 Script executed:

# Examine the actual script and Set-StrictMode setting
cat -n tools/validate-repro-bundle.ps1 | head -100

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 4536


🏁 Script executed:

# Find validation-helpers.ps1 and check Require-ValidationField implementation
find . -name "validation-helpers.ps1" -type f

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 99


🏁 Script executed:

# Search for any old Require-Field usage or definition
rg -n "Require-Field" tools/ --type ps1 -A 3

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 96


🏁 Script executed:

# Read validation-helpers.ps1 to see Require-ValidationField implementation
cat -n tools/validation-helpers.ps1

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 2307


🏁 Script executed:

# Search for old Require-Field code (without type filter)
rg -n "Require-Field" tools/

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 534


🏁 Script executed:

# Check the fixture file to see if launchContext, runtimeMode, diagnostics are always present
cat -n tools/fixtures/repro_bundle_sample.json

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 1537


🏁 Script executed:

# Check if there's any guard before lines 69-79, and see the old Require-Field implementation
sed -n '28,50p' tools/validate-save-patch-pack.ps1

Repository: Prekzursil/SWFOC-Mod-Menu

Length of output: 616


Accessing absent nested objects directly throws PropertyNotFoundException under strict mode.

Lines 70, 74, and 78 each pass $bundle.launchContext, $bundle.runtimeMode, and $bundle.diagnostics directly as the -Object parameter to Require-ValidationField. Under Set-StrictMode -Version Latest (line 7), if any of these nested objects are absent from the JSON bundle, the property access itself throws a terminating PropertyNotFoundException at the call site — before Require-ValidationField is ever invoked — bypassing validation error collection and crashing the script.

The old Require-Field implementation (in tools/validate-save-patch-pack.ps1) had an explicit null guard (if ($null -eq $Object)), which the new Require-ValidationField lacks. The fixture file (tools/fixtures/repro_bundle_sample.json) contains all these nested fields, so the CI smoke test does not expose this latent bug; it will only surface when a malformed bundle is validated.

Use the safe PSObject.Properties pattern to extract nested objects before passing them to validation:

🛡️ Proposed fix for lines 69–79
-foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
-    Require-ValidationField -Object $bundle.launchContext -Field $required -Errors $errors -AllowNull
-}
-
-foreach ($required in @("hint", "effective", "reasonCode")) {
-    Require-ValidationField -Object $bundle.runtimeMode -Field $required -Errors $errors
-}
-
-foreach ($required in @("dependencyState", "helperReadiness", "symbolHealthSummary")) {
-    Require-ValidationField -Object $bundle.diagnostics -Field $required -Errors $errors
-}
+$launchContext = $bundle.PSObject.Properties['launchContext']?.Value
+foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
+    Require-ValidationField -Object $launchContext -Field $required -Errors $errors -AllowNull
+}
+
+$runtimeMode = $bundle.PSObject.Properties['runtimeMode']?.Value
+foreach ($required in @("hint", "effective", "reasonCode")) {
+    Require-ValidationField -Object $runtimeMode -Field $required -Errors $errors
+}
+
+$diagnostics = $bundle.PSObject.Properties['diagnostics']?.Value
+foreach ($required in @("dependencyState", "helperReadiness", "symbolHealthSummary")) {
+    Require-ValidationField -Object $diagnostics -Field $required -Errors $errors
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
Require-Field -Object $bundle.launchContext -Field $required -AllowNull
Require-ValidationField -Object $bundle.launchContext -Field $required -Errors $errors -AllowNull
}
foreach ($required in @("hint", "effective", "reasonCode")) {
Require-Field -Object $bundle.runtimeMode -Field $required
Require-ValidationField -Object $bundle.runtimeMode -Field $required -Errors $errors
}
foreach ($required in @("dependencyState", "helperReadiness", "symbolHealthSummary")) {
Require-Field -Object $bundle.diagnostics -Field $required
Require-ValidationField -Object $bundle.diagnostics -Field $required -Errors $errors
}
foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
Require-ValidationField -Object $bundle.launchContext -Field $required -Errors $errors -AllowNull
}
$launchContext = $bundle.PSObject.Properties['launchContext']?.Value
foreach ($required in @("profileId", "reasonCode", "confidence", "launchKind")) {
Require-ValidationField -Object $launchContext -Field $required -Errors $errors -AllowNull
}
$runtimeMode = $bundle.PSObject.Properties['runtimeMode']?.Value
foreach ($required in @("hint", "effective", "reasonCode")) {
Require-ValidationField -Object $runtimeMode -Field $required -Errors $errors
}
$diagnostics = $bundle.PSObject.Properties['diagnostics']?.Value
foreach ($required in @("dependencyState", "helperReadiness", "symbolHealthSummary")) {
Require-ValidationField -Object $diagnostics -Field $required -Errors $errors
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-repro-bundle.ps1` around lines 69 - 79, The code currently
accesses nested properties ($bundle.launchContext, $bundle.runtimeMode,
$bundle.diagnostics) directly under Set-StrictMode which throws
PropertyNotFoundException if those keys are missing; change the calls so you
first safely extract each nested object using the PSObject.Properties/null-check
pattern (e.g., $bundle.PSObject.Properties['launchContext']?.Value) or test for
$null before passing to Require-ValidationField, and ensure
Require-ValidationField is invoked only when the extracted value is non-null (or
pass $null explicitly if AllowNull is intended) so validation errors are
collected instead of the script terminating; update the call sites that
reference Require-ValidationField accordingly.

Comment on lines +42 to +47
$warnings = @($manifest.warnings)
for ($i = 0; $i -lt $warnings.Count; $i++) {
if ($null -eq $warnings[$i]) {
Add-ValidationError -Errors $errors -Message "warnings[$i] must not be null"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@($manifest.warnings) produces a false positive when warnings is absent or null.

@($null) in PowerShell creates a one-element array containing $null, not an empty array. If warnings is an optional field that is absent from the JSON (or explicitly null), the loop iterates once and emits warnings[0] must not be null — a spurious error. Additionally, under Set-StrictMode -Version Latest, accessing $manifest.warnings when warnings is not a JSON key at all throws a PropertyNotFoundException before the array wrapping even occurs.

🛡️ Proposed fix
-$warnings = @($manifest.warnings)
-for ($i = 0; $i -lt $warnings.Count; $i++) {
-    if ($null -eq $warnings[$i]) {
-        Add-ValidationError -Errors $errors -Message "warnings[$i] must not be null"
-    }
-}
+$warningsProp = $manifest.PSObject.Properties['warnings']
+if ($null -ne $warningsProp -and $null -ne $warningsProp.Value) {
+    $warnings = @($warningsProp.Value)
+    for ($i = 0; $i -lt $warnings.Count; $i++) {
+        if ($null -eq $warnings[$i]) {
+            Add-ValidationError -Errors $errors -Message "warnings[$i] must not be null"
+        }
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$warnings = @($manifest.warnings)
for ($i = 0; $i -lt $warnings.Count; $i++) {
if ($null -eq $warnings[$i]) {
Add-ValidationError -Errors $errors -Message "warnings[$i] must not be null"
}
}
$warningsProp = $manifest.PSObject.Properties['warnings']
if ($null -ne $warningsProp -and $null -ne $warningsProp.Value) {
$warnings = @($warningsProp.Value)
for ($i = 0; $i -lt $warnings.Count; $i++) {
if ($null -eq $warnings[$i]) {
Add-ValidationError -Errors $errors -Message "warnings[$i] must not be null"
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validate-support-bundle-manifest.ps1` around lines 42 - 47, Avoid
accessing $manifest.warnings directly when the key may be missing and prevent
@($null) producing a one-element null array: first check whether the manifest
actually contains the "warnings" property and that it is not $null (e.g. if
($manifest.PSObject.Properties.Name -contains 'warnings' -and $manifest.warnings
-ne $null) { $warnings = @($manifest.warnings) } else { $warnings = @() }), then
iterate $warnings and call Add-ValidationError as before; this ensures no
PropertyNotFoundException under Set-StrictMode and avoids the false-positive
"warnings[0] must not be null".

Comment on lines +7 to +14
function Add-ValidationError {
param(
[Parameter()][System.Collections.ArrayList]$Errors,
[Parameter(Mandatory = $true)][string]$Message
)

[void]$Errors.Add($Message)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

$Errors should be Mandatory = $true across all three consumer functions.

All three functions — Add-ValidationError (Line 9), Require-ValidationField (Line 20), and Write-ValidationResult (Line 46) — declare $Errors without [Parameter(Mandatory = $true)]. Under Set-StrictMode -Version Latest, a caller that accidentally omits -Errors gets $null, and the first .Add() or .Count access becomes a terminating NullReferenceException rather than a clear parameter-missing error.

🛡️ Proposed fix (apply to all three functions)
 function Add-ValidationError {
     param(
-        [Parameter()][System.Collections.ArrayList]$Errors,
+        [Parameter(Mandatory = $true)][System.Collections.ArrayList]$Errors,
         [Parameter(Mandatory = $true)][string]$Message
     )
 function Require-ValidationField {
     param(
         [Parameter(Mandatory = $true)][object]$Object,
         [Parameter(Mandatory = $true)][string]$Field,
-        [Parameter()][System.Collections.ArrayList]$Errors,
+        [Parameter(Mandatory = $true)][System.Collections.ArrayList]$Errors,
 function Write-ValidationResult {
     param(
-        [Parameter()][System.Collections.ArrayList]$Errors,
+        [Parameter(Mandatory = $true)][System.Collections.ArrayList]$Errors,

Also applies to: 16-23, 44-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/validation-helpers.ps1` around lines 7 - 14, All three functions
(Add-ValidationError, Require-ValidationField, Write-ValidationResult) currently
accept $Errors without marking it mandatory; under Set-StrictMode this can yield
a null and cause a NullReferenceException on .Add()/.Count. Update the parameter
declarations for $Errors in each function to include [Parameter(Mandatory =
$true)] while keeping the existing type ([System.Collections.ArrayList]) so
callers get a clear missing-parameter error instead of a runtime null
dereference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:app WPF app/UI/ViewModel area:ci CI/CD and automation area:docs Documentation/process related work. area:profiles Profile metadata, inheritance, manifest area:tooling Scripts and developer tooling needs-reviewer No eligible non-author reviewer available Review effort 4/5

Projects

None yet

1 participant

Comments