Skip to content

feat(runtime): Wave 17.2 — Runtime Integrity#1

Closed
millsydotdev wants to merge 1 commit into
mainfrom
wave-17.2
Closed

feat(runtime): Wave 17.2 — Runtime Integrity#1
millsydotdev wants to merge 1 commit into
mainfrom
wave-17.2

Conversation

@millsydotdev

@millsydotdev millsydotdev commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Three runtime integrity improvements:

Task 1 — Suspension Compression

  • Zstd snapshot compression integrated
  • Backward-compatible via Zstd magic byte detection
  • Performance benchmark verifies <500ms suspend / <1000ms resume targets
  • 3 new tests

Task 2 — SDK Panic Elimination

  • Removed all .expect()\ panic paths from SDK (8 calls fixed)
  • Mutex recovery via \into_inner()\ — games never crash on lock poison
  • Consistent \lock_storage/ \lock_rng\ helper pattern

Task 3 — Asset Pipeline Hardening

  • Replaced .expect()\ on texture_loader lock with mutex recovery
  • 12 cache lock calls hardened with \lock_entries/ \lock_id_map\ helpers
  • All public-facing asset API paths now use poison recovery

Validation

  • cargo fmt --check: clean
  • cargo clippy -D warnings: clean
  • cargo test: 79 tests pass (52 asset + 13 SDK + 14 suspension)
  • cargo build --workspace: clean

Summary by CodeRabbit

  • New Features

    • Added a new asset system for loading, caching, and managing textures, audio, Lua, raw files, and packages.
    • Introduced richer audio controls, including per-channel volume, mute, playback handles, and cached sound playback.
    • Expanded configuration with graphics, input, audio, developer, and profile support.
    • Added runtime diagnostics, service tracking, scene state persistence, and library/store management improvements.
  • Bug Fixes

    • Improved state handling across app, scene, input, and runtime flows for more reliable startup, shutdown, and recovery.
    • Added safer IPC, renderer, and sandbox behavior for more stable execution.

…tion, asset hardening

Complete three runtime integrity tasks:

Task 1 — Suspension system completion:
- Zstd snapshot compression with magic-byte detection for backward compat
- Performance benchmark test verifies <500ms suspend, <1000ms resume targets
- 3 new regression tests (compressed roundtrip, size comparison, perf)

Task 2 — SDK panic elimination:
- Removed 5 expect("storage lock") calls via mutex recovery helper
- Removed 3 expect("rng lock") calls via mutex recovery helper
- All SDK functions now recover gracefully from mutex poison

Task 3 — Asset pipeline hardening:
- Replaced expect("texture_loader lock") with mutex recovery
- Replaced 12 expect("cache lock") calls with recovery helpers
- Added lock_entries/lock_id_map helpers for consistent mutex handling

All 79 tests pass. cargo fmt, clippy -D warnings clean.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new vibege-asset crate (caching, loading, package mounting) integrated into audio, renderer, and scene crates. It refactors vibege-audio (mixer/sound cache/playback handles), vibege-config (versioned schema/migration/profiles), vibege-core (diagnostics/services/state machine/event bus), vibege-input (action mapping/gamepad/mouse), vibege-ipc (TCP transport), vibege-renderer (batched pipeline), and vibege-sandbox (Windows Job Objects). vibege-scene gains a game runtime/lifecycle framework, expanded SceneManager (overlays/persistence/messaging), and new library/store platform modules, all wired through the runtime-app binary.

Changes

Asset Management System

Layer / File(s) Summary
Manifest & registration
Cargo.toml, crates/vibege-asset/Cargo.toml
Registers the new vibege-asset crate in the workspace with its dependencies.
Handles & lifetimes
crates/vibege-asset/src/handle.rs
AssetId, reference-counted ResourceLifetime, and AssetHandle clone/drop semantics.
Metadata & asset types
crates/vibege-asset/src/metadata.rs, crates/vibege-asset/src/types.rs
AssetTypeId/AssetSource/AssetMetadata plus Texture/Audio/Font/LuaSource/Raw/Package asset structs.
Cache & statistics
crates/vibege-asset/src/cache.rs, crates/vibege-asset/src/statistics.rs
Deduplicating AssetCache with hit/miss/load/release counters and aggregated TypeStats/AssetStatistics.
Loaders & package mounting
crates/vibege-asset/src/loader.rs, crates/vibege-asset/src/package.rs
TextureLoader/AudioLoader/RawLoader/LuaSourceLoader and ZIP-based PackageMount.
AssetManager facade
crates/vibege-asset/src/lib.rs
Top-level load/get/release/statistics/clear API across all asset caches.

Audio Engine Refactor

Layer / File(s) Summary
Module split & errors
crates/vibege-audio/Cargo.toml, crates/vibege-audio/src/lib.rs
Splits into engine/handle/mixer/sound_cache modules and adds AudioError.
Mixer & playback handle
crates/vibege-audio/src/mixer.rs, crates/vibege-audio/src/handle.rs
Per-channel mixer with looping/mute, and PlaybackHandle control API.
Sound cache & engine
crates/vibege-audio/src/sound_cache.rs, crates/vibege-audio/src/engine.rs
Key-addressed PCM cache and AudioSystem wiring rodio with asset-based playback.

Core Runtime Enhancements

Layer / File(s) Summary
Diagnostics & services
crates/vibege-core/src/diagnostics.rs, crates/vibege-core/src/services.rs
Health reporting collector and dependency-ordered ServiceRegistry.
State machine & event bus
crates/vibege-core/src/state_machine.rs, crates/vibege-core/src/event.rs, crates/vibege-core/src/error.rs
RuntimeState transitions and prioritized, metric-tracked EventBus.
Lifecycle integration
crates/vibege-core/src/lifecycle.rs, crates/vibege-core/src/lib.rs, crates/vibege-core/src/metrics.rs
App run/shutdown driven by StateMachine plus updated exports/lock handling.

Config System Refactor

Layer / File(s) Summary
Validate trait & sections
crates/vibege-config/src/validation.rs, crates/vibege-config/src/config/*.rs
Validate trait plus Audio/Developer/Graphics/Input/Overlay/General config sections.
Migration & profiles
crates/vibege-config/src/migration.rs, crates/vibege-config/src/profile.rs
Version migration and built-in profile snapshots.
ConfigHandle & tests
crates/vibege-config/src/handle.rs, crates/vibege-config/src/lib.rs, crates/vibege-config/tests/integration.rs
Persistence/export/import/backup/profile API plus integration tests.

Input System Refactor

Layer / File(s) Summary
Action/context/gamepad/mouse
crates/vibege-input/src/action.rs, context.rs, gamepad.rs, mouse.rs
ActionMap bindings, layered ContextStack, multi-slot gamepad, and mouse double-click/drag tracking.
InputManager composition
crates/vibege-input/src/lib.rs, crates/vibege-input/Cargo.toml
Composes new state modules with focus tracking and reworked event handling.

IPC TCP Transport Rewrite

Layer / File(s) Summary
Transport & messages
crates/vibege-ipc/src/lib.rs, crates/vibege-ipc/Cargo.toml
JSON-framed length-prefixed TCP transport replacing the simulated bridge.

Renderer Batching Pipeline

Layer / File(s) Summary
Infra & init
crates/vibege-renderer/src/lib.rs, crates/vibege-renderer/Cargo.toml
NdcConverter, StagingBatch, TextureSlotManager, and asset-based texture loading.
Render loop & tests
crates/vibege-renderer/src/lib.rs
Batched draw loop with surface-loss recovery and corresponding tests.

Sandbox Windows Job Object Isolation

Layer / File(s) Summary
Job Object spawn & lifecycle
crates/vibege-sandbox/src/lib.rs, crates/vibege-sandbox/Cargo.toml
Windows Job Object creation/assignment with kill-on-close and a Unix env-var stub.

Game Runtime, Library & Store Framework (vibege-scene)

Layer / File(s) Summary
Scene core & manager
crates/vibege-scene/src/scene/*.rs
SceneKind/SceneMessage/state, and SceneManager with overlays/persistence/messaging.
Runtime framework
crates/vibege-scene/src/runtime/*.rs
RuntimeContext/RuntimeState/SessionController/GameRuntime/PackageValidator.
Game/Home/Error scenes
crates/vibege-scene/src/scenes/game_scene.rs, home_scene.rs, error_scene.rs, game_manager.rs
Wires scenes to assets, suspension snapshots, and Lua sandboxing.
Library & Store platforms
crates/vibege-scene/src/library/*.rs, crates/vibege-scene/src/store/*.rs, library_scene.rs, store_scene.rs
LibraryManager/StoreManager backends and their scene UI integration.

Runtime App Wiring

Layer / File(s) Summary
Startup & event loop
crates/vibege-runtime-app/src/main.rs, crates/vibege-runtime-app/Cargo.toml
Wires diagnostics, service registry, asset manager, and overlay persistence into the main loop.

Sequence Diagram(s)

sequenceDiagram
  participant GameScene
  participant GameRuntime
  participant SessionController
  participant AssetManager
  participant PackageValidator

  GameScene->>GameRuntime: load_from_package(data, game_name)
  GameRuntime->>AssetManager: mount_package(key, data)
  AssetManager-->>GameRuntime: PackageAsset
  GameRuntime->>PackageValidator: validate(manifest, entry_data, engine_version)
  PackageValidator-->>GameRuntime: ValidationReport
  GameRuntime->>SessionController: mount / initialize / start
  SessionController->>AssetManager: load_lua_source / load_texture
  SessionController-->>GameRuntime: Running
  GameRuntime-->>GameScene: &mut SessionController
Loading
sequenceDiagram
  participant Renderer
  participant TextureSlotManager
  participant AssetManager
  participant StagingBatch
  participant GPUQueue

  Renderer->>AssetManager: load_texture_asset(data)
  AssetManager-->>Renderer: TextureAsset
  Renderer->>TextureSlotManager: allocate bind group slot
  Renderer->>StagingBatch: ensure(vertex/index capacity)
  Renderer->>StagingBatch: upload(vertices, indices)
  Renderer->>GPUQueue: draw_indexed per BindGroupId batch
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

Poem

A rabbit hops through crates anew,
Caching textures, sound, and Lua too 🐇
Mixers hum and scenes now stack,
Sandboxed jobs keep games on track.
From asset burrow to render's light,
This warren's runtime took deep flight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly aligned with the PR’s runtime integrity-focused changes across compression, panic elimination, and asset hardening.
Docstring Coverage ✅ Passed Docstring coverage is 89.06% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-17.2

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
crates/vibege-input/src/lib.rs (1)

438-473: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Advance MouseState each frame before clearing mouse queries.

MouseState::tick() is never called, so double_click_timer never expires and double_click is never cleared. After one click, any later second click can register as a double-click, and is_double_click() can remain true across frames.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-input/src/lib.rs` around lines 438 - 473, The mouse frame
advance logic in end_frame currently clears deltas and button states but never
advances MouseState, so double_click_timer can linger and double_click may stay
set across frames. Update end_frame to call MouseState::tick() as part of the
per-frame cleanup before clearing mouse queries, using the existing MouseState
type and its tick method so double-click state expires correctly each frame.
crates/vibege-scene/src/scenes/settings_scene.rs (1)

85-107: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the existing config instead of rebuilding sections from defaults.

This now resets unrelated saved preferences on every Settings save. AudioConfig.muted/music_volume/sfx_volume and GeneralConfig.theme are not copied here, so they silently fall back to defaults.

💾 Suggested fix
 fn save(&mut self, ctx: &mut SceneContext) {
-    ctx.config.set(vibege_config::VibegeConfig {
-        overlay: vibege_config::OverlayConfig {
-            hotkey_modifiers: self.hotkey_mod.clone(),
-            hotkey_key: self.hotkey_key.clone(),
-            position: self.position.clone(),
-            width: 800,
-            height: 600,
-            ..Default::default()
-        },
-        audio: vibege_config::AudioConfig {
-            volume: self.volume,
-            ..Default::default()
-        },
-        general: vibege_config::GeneralConfig {
-            startup_behavior: self.startup.clone(),
-            performance_mode: self.perf.clone(),
-            first_run_complete: true,
-            backend_url: "http://localhost:3000/api/v1".into(),
-            ..Default::default()
-        },
-        ..Default::default()
-    });
+    let mut config = ctx.config.get();
+    config.overlay.hotkey_modifiers = self.hotkey_mod.clone();
+    config.overlay.hotkey_key = self.hotkey_key.clone();
+    config.overlay.position = self.position.clone();
+    config.overlay.width = 800;
+    config.overlay.height = 600;
+    config.audio.volume = self.volume;
+    config.general.startup_behavior = self.startup.clone();
+    config.general.performance_mode = self.perf.clone();
+    config.general.first_run_complete = true;
+    config.general.backend_url = "http://localhost:3000/api/v1".into();
+    config.general.theme = self.theme.clone();
+    ctx.config.set(config);
     self.dirty = false;
     info!("Settings saved");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/scenes/settings_scene.rs` around lines 85 - 107, The
save logic in settings_scene::SettingsScene::save is rebuilding VibegeConfig
from defaults, which wipes unrelated persisted values. Update this method to
start from the existing config in SceneContext and only overwrite the fields
edited in the settings UI, especially preserving
AudioConfig.muted/music_volume/sfx_volume and GeneralConfig.theme while still
updating overlay, volume, startup_behavior, performance_mode,
first_run_complete, and backend_url.
crates/vibege-sandbox/src/lib.rs (3)

112-131: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against spawning over an existing child.

Line 112 allows spawn() to be called again while a previous Unix child is still running; replacing self.child drops the handle without killing the process.

Suggested guard
 pub fn spawn(&mut self) -> Result<()> {
+    if self.is_running() {
+        return Err(RuntimeError::new(
+            ErrorCode::INVALID_STATE_TRANSITION,
+            "Sandbox is already running",
+        ));
+    }
     let config = &self.config;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-sandbox/src/lib.rs` around lines 112 - 131, Guard against
calling spawn() when a child process is already active, since replacing
self.child can orphan a running Unix process. Add an early check in
Sandbox::spawn (before spawn_unix()/spawn_windows()) that verifies self.child is
None or the existing Child has exited, and return an error if a sandbox is
already running. Keep the fix localized to spawn() and the child-management
logic so the handle is never overwritten while a process is still alive.

335-339: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Terminate the Windows job during shutdown. child.kill() only stops the main process; the Job Object stays open until Sandbox drops, so descendant processes can keep running after request_shutdown(). Close the job handle here or call TerminateJobObject to stop the whole tree immediately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-sandbox/src/lib.rs` around lines 335 - 339, The shutdown path
in request_shutdown only kills the child process and leaves the Windows Job
Object alive, so descendant processes may continue running until Sandbox is
dropped. Update the request_shutdown implementation to also terminate or close
the job for the sandboxed process, using the existing Sandbox job-management
logic/handle in this module, and keep the shutdown logging intact. Make sure the
fix targets the same child/job state managed by Sandbox so the entire process
tree stops immediately on shutdown.

249-277: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Assign the child in a suspended state first. Command::spawn() starts the process before AssignProcessToJobObject, leaving a window where game code and any descendants can run outside the job. Use suspended creation on Windows, assign the job, then resume.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-sandbox/src/lib.rs` around lines 249 - 277, In the process
startup flow in vibege-sandbox’s `lib.rs`, `Command::spawn()` starts the child
before `AssignProcessToJobObject`, leaving it briefly outside the job. Change
the Windows launch path to create the process suspended first, then call
`AssignProcessToJobObject` on the child handle, and only after that resume the
process. Keep the cleanup/error handling around the existing `cmd.spawn`,
`child.id()`, and `AssignProcessToJobObject` steps consistent with the current
`RuntimeError::with_cause` flow.
🟡 Minor comments (12)
crates/vibege-audio/src/mixer.rs-109-112 (1)

109-112: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Recover poisoned mixer locks instead of panicking. lock().expect("mixer lock") is repeated throughout mixer.rs; if one audio path panics while holding a mutex, the next mixer call will panic too. Recover the poisoned guard here so the audio path stays non-fatal, matching the crate’s error model. crates/vibege-audio/src/mixer.rs:111

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-audio/src/mixer.rs` around lines 109 - 112, The mixer lock
handling in set_volume currently panics on a poisoned mutex via the mixer lock
expect path, which makes subsequent audio calls fail non-fatally. Update the
lock acquisition in set_volume to recover from a poisoned guard instead of
panicking, and apply the same recovery pattern used by other mixer.rs methods
that access self.channels so the mixer stays usable after a prior panic.
crates/vibege-renderer/src/lib.rs-871-875 (1)

871-875: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check ch.is_ascii() before converting to u8. char as u8 truncates non-ASCII input (e.g. 🙂 becomes B), so some unsupported characters can render as a random printable glyph instead of the space fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-renderer/src/lib.rs` around lines 871 - 875, The text rendering
loop in the draw-list code is converting each character with a direct `char as
u8` cast before checking the ASCII range, which can turn non-ASCII input into a
random printable glyph. Update the character handling in the `self.draw_list`
text iteration so `ch.is_ascii()` is checked before any `u8` conversion, and
only cast ASCII characters while sending all unsupported characters to the space
fallback.
crates/vibege-input/src/context.rs-93-100 (1)

93-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

find() / find_mut() search the wrong end of the stack.

The docs say these return the top context, but iter().find(...) returns the oldest matching entry. If the same context name is pushed more than once, reads and mutations hit the wrong layer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-input/src/context.rs` around lines 93 - 100, The
`ContextStack::find` and `ContextStack::find_mut` methods are scanning from the
oldest entry, but they are documented to return the top context. Update both
methods in `context.rs` to search from the end of `self.contexts` so the most
recently pushed `InputContext` with the matching name is returned for both read
and mutable access.
crates/vibege-input/src/mouse.rs-36-38 (1)

36-38: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Double-click timing needs to be per button.

double_click_timer is shared across all buttons, so a left-click can arm a right-click double-click. The public query is per button, so this creates false positives across unrelated mouse buttons.

Also applies to: 75-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-input/src/mouse.rs` around lines 36 - 38, The double-click
state in Mouse is currently shared across all buttons via double_click_timer,
which can cause one button press to affect another. Update the mouse
double-click tracking in Mouse and any related handling methods to store timing
per MouseButton, matching the existing per-button double_click map. Make sure
the logic that sets, checks, and clears double-click state uses the button
identity so left/right clicks cannot arm each other.
crates/vibege-scene/src/library/integrity.rs-82-102 (1)

82-102: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require real files for manifest and entry point checks.

exists() lets a directory named .vibege-install.json or main.lua pass integrity, but launch later expects a readable source file. Use is_file() for both checks.

Proposed fix
-        if meta_path.exists() {
+        if meta_path.is_file() {
...
-        if entry_path.exists() {
+        if entry_path.is_file() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/library/integrity.rs` around lines 82 - 102, The
integrity checks in check_manifest_exists and check_entry_point_exists currently
accept directories because they use exists(), which can let a folder named
.vibege-install.json or the entry point path pass validation. Update both checks
to require real files by using is_file() on meta_path and entry_path, while
keeping the existing IntegrityReport and IntegrityCheck behavior unchanged.
crates/vibege-config/src/handle.rs-296-307 (1)

296-307: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Notify watchers after create_profile().

create_profile() mutates config.profiles and writes it to disk, but unlike the other mutators it never calls notify(). Any UI/state driven by on_change() will miss newly created profiles until some unrelated config change happens later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-config/src/handle.rs` around lines 296 - 307, The
create_profile mutator updates config.profiles and saves to disk but never
notifies listeners, so on_change-driven UI/state can miss the new profile.
Update create_profile in handle.rs to call notify() after the config is
successfully mutated and persisted, matching the behavior of the other mutators
that broadcast changes.
crates/vibege-config/src/handle.rs-254-259 (1)

254-259: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep dirty false after a successful profile save.

These paths persist immediately, but they leave dirty = true, so is_dirty() keeps reporting unsaved changes even after the write succeeded. That breaks the contract documented on Lines 81-88.

Also applies to: 275-280, 304-307, 320-325

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-config/src/handle.rs` around lines 254 - 259, The profile save
paths are marking the config as dirty before writing and never clearing it after
a successful save, so `is_dirty()` stays true even though persistence succeeded.
Update the save flow in `save_profile` and the other immediate-save branches
referenced by the same pattern to only keep `dirty` set on failure, and
explicitly reset `guard.dirty` to false after `save_config_file` returns
successfully while preserving the existing error handling and state cloning
around `guard`, `path`, and `cfg`.
crates/vibege-asset/src/statistics.rs-38-46 (1)

38-46: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Merge the per-type breakdown as well.

merge() updates the totals but drops asset_type_breakdown, so merged stats can no longer reconcile with their per-type detail.

Suggested fix
 pub fn merge(&mut self, other: &AssetStatistics) {
     self.total_assets += other.total_assets;
     self.total_memory_bytes += other.total_memory_bytes;
     self.total_cache_hits += other.total_cache_hits;
     self.total_cache_misses += other.total_cache_misses;
     self.total_loads += other.total_loads;
     self.total_releases += other.total_releases;
     self.total_failed_loads += other.total_failed_loads;
+
+    for (&asset_type, stats) in &other.asset_type_breakdown {
+        let entry = self.asset_type_breakdown.entry(asset_type).or_default();
+        entry.count += stats.count;
+        entry.memory_bytes += stats.memory_bytes;
+        entry.cache_hits += stats.cache_hits;
+        entry.cache_misses += stats.cache_misses;
+        entry.loads += stats.loads;
+        entry.releases += stats.releases;
+        entry.failed_loads += stats.failed_loads;
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-asset/src/statistics.rs` around lines 38 - 46, The
AssetStatistics::merge method currently combines only the aggregate counters and
leaves asset_type_breakdown behind, so merged statistics lose their per-type
detail. Update merge to also merge the breakdown map for each asset type,
ensuring counts are accumulated rather than overwritten, while keeping the
existing total fields behavior unchanged.
crates/vibege-asset/src/cache.rs-100-106 (1)

100-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Count misses in get_data() too.

This accessor updates hits but never updates misses, so cache hit-rate reporting is wrong for callers that probe through get_data().

Suggested fix
 pub fn get_data(&self, key: &str) -> Option<T> {
     let entries = lock_entries(&self.entries);
-    entries.get(key).map(|e| {
-        self.hits.fetch_add(1, Ordering::Relaxed);
-        e.data.clone()
-    })
+    match entries.get(key) {
+        Some(e) => {
+            self.hits.fetch_add(1, Ordering::Relaxed);
+            Some(e.data.clone())
+        }
+        None => {
+            self.misses.fetch_add(1, Ordering::Relaxed);
+            None
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-asset/src/cache.rs` around lines 100 - 106, The get_data method
in Cache only increments hits on a successful lookup and never records misses,
so probe-based access skews hit-rate reporting. Update Cache::get_data to
increment the misses counter when entries.get(key) returns None, keeping the
existing hits increment on successful clones, and preserve the current locking
and Ordering::Relaxed usage around the entries and counters.
crates/vibege-core/src/event.rs-150-180 (1)

150-180: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Priority is only enforced within each subscriber list.

The API says higher-priority subscribers receive events first, but publish() always runs subscribers before filtered. That means a Low global subscriber still beats a High filtered subscriber for the same event. Either merge dispatch by priority or narrow the public contract/docs.

Also applies to: 185-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-core/src/event.rs` around lines 150 - 180, The event dispatch
order in RuntimeEvent handling is inconsistent because `publish()` processes the
`subscribers` list before the `filtered` list, so a lower-priority global
subscriber can run ahead of a higher-priority filtered subscriber. Fix this by
either merging the two subscription paths into a single priority-ordered
dispatch in `publish()` (using the existing `subscribe_with_priority` and
`subscribe_filtered_with_priority` priority values) or by updating the public
contract/docs to clearly state that priority only applies within each list.
crates/vibege-scene/src/store/download.rs-103-107 (1)

103-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

all() doesn't include the active download.

The doc says this returns queued and active tasks, but it only clones queue. Any UI polling all() will lose the currently downloading item as soon as next() moves it out of the deque.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/store/download.rs` around lines 103 - 107, The
DownloadStore::all method currently returns only the queued items from queue and
omits the active download, so it does not match its documented behavior. Update
all() to include the currently active task as well, using the store’s
active/current download state together with queue, so callers polling all() can
still see the in-progress item after next() removes it from the deque.
crates/vibege-scene/src/store/cache.rs-54-60 (1)

54-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset cache metadata when the cache contents are refreshed or cleared.

The fallback path in StoreManager::fetch marks the cache offline, but a later successful cache_listings() call never clears that flag. clear() also leaves last_fetch untouched, so is_offline() / has_recent_data() can report stale state after recovery or after a manual cache clear.

Proposed fix
 pub fn cache_listings(&self, listings: Vec<GameListing>, ttl_secs: u64) {
     let mut cache = self.listings.lock().expect("cache lock");
     let ttl = Duration::from_secs(ttl_secs);
     for listing in listings {
         cache.insert(listing.id.clone(), CachedEntry::new(listing, ttl));
     }
     *self.last_fetch.lock().expect("cache lock") = Some(Instant::now());
+    *self.offline.lock().expect("cache lock") = false;
 }
@@
     pub fn clear(&self) {
         self.listings.lock().expect("cache lock").clear();
         self.search_results.lock().expect("cache lock").clear();
         self.sections.lock().expect("cache lock").clear();
+        *self.last_fetch.lock().expect("cache lock") = None;
+        *self.offline.lock().expect("cache lock") = false;
     }

Also applies to: 149-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/store/cache.rs` around lines 54 - 60, Reset the cache
metadata whenever the cache is refreshed or cleared: `cache_listings()` should
also clear the offline/stale state that `StoreManager::fetch` sets, and
`clear()` should reset `last_fetch` along with the cached listings. Update the
`Cache` methods `cache_listings` and `clear` so `is_offline()` and
`has_recent_data()` reflect the current cache contents after recovery or a
manual clear. Make sure the reset happens in the same place the listings map is
updated, using the existing `last_fetch` and any offline flag fields tracked by
`Cache`.
🧹 Nitpick comments (5)
crates/vibege-ipc/src/lib.rs (1)

455-461: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the size-limit error path.

This only proves the fixture is large; it does not prove write_message_to rejects oversized messages.

🧪 Proposed test tightening
     fn test_message_size_limit() {
         let transport = create_test_transport();
         let conn = IpcConnection::new(transport).with_max_message_size(10);
         let msg = IpcMessage::new(MessageKind::Ping, "x".repeat(100).as_str());
-        let json = serde_json::to_vec(&msg).unwrap();
-        assert!(json.len() as u64 > conn.max_message_size);
+
+        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+        let addr = listener.local_addr().unwrap();
+        let handle = std::thread::spawn(move || {
+            let _ = listener.accept();
+        });
+
+        let mut stream = TcpStream::connect(addr).unwrap();
+        let result = write_message_to(&mut stream, &msg, conn.max_message_size, conn.stats());
+        assert!(result.is_err());
+        handle.join().unwrap();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-ipc/src/lib.rs` around lines 455 - 461, The current test only
checks that a serialized `IpcMessage` is larger than
`IpcConnection::max_message_size`, but it does not exercise the actual rejection
path in `write_message_to`. Update `test_message_size_limit` in
`crates/vibege-ipc/src/lib.rs` to send an oversized message through
`IpcConnection::write_message_to` (using the existing `create_test_transport`,
`IpcConnection::new`, and `IpcMessage::new` setup) and assert that the
size-limit error is returned instead of only comparing lengths.
crates/vibege-runtime-app/src/main.rs (1)

371-377: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Debounce the overlay hotkey
GetAsyncKeyState only reports the current key-down state here, so holding the combo re-arms request_toggle() on every AboutToWait pass and can flip the overlay repeatedly. Track the previous pressed state and toggle only on the up→down edge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-runtime-app/src/main.rs` around lines 371 - 377, The overlay
hotkey handling in main’s key-check loop currently triggers request_toggle() on
every AboutToWait pass while the combo remains held. Update the hotkey logic to
remember the previous pressed state for the matched key combo and only call
vibege_tray::request_toggle() when it transitions from not pressed to pressed.
Use the existing key-state checks around GetAsyncKeyState, and add a small
debouncing state near the hotkey handling in main.
crates/vibege-scene/src/library/collections.rs (1)

28-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Lock poisoning will permanently break CollectionManager after any panic.

Every method does self.collections.lock().expect("collections lock"). If any critical section ever panics (even from an unrelated bug), the mutex becomes poisoned and every subsequent call to rebuild, all, add_to_collection, etc. will panic forever, taking down the whole collections subsystem. Recovering with unwrap_or_else(|e| e.into_inner()) avoids this cascade.

♻️ Example fix pattern
-    pub fn all(&self) -> Vec<Collection> {
-        self.collections.lock().expect("collections lock").clone()
-    }
+    pub fn all(&self) -> Vec<Collection> {
+        self.collections
+            .lock()
+            .unwrap_or_else(|e| e.into_inner())
+            .clone()
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/library/collections.rs` around lines 28 - 133, The
`CollectionManager` methods are using
`self.collections.lock().expect("collections lock")`, which turns any mutex
poisoning into a permanent panic cascade across `rebuild`, `all`, `get`,
`add_custom`, `remove_custom`, `add_to_collection`, `remove_from_collection`,
`is_favorite`, and `pinned_games`. Update each lock acquisition in
`CollectionManager` to recover from poisoning by taking the inner value from the
poisoned lock instead of expecting success, so the subsystem keeps working after
an unrelated panic.
crates/vibege-scene/src/library/registry.rs (1)

107-120: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use poison recovery for registry locks.

This new runtime path still uses .expect("registry lock"), so one poisoned mutex turns later library operations into panics instead of recovering with into_inner() like the PR’s lock-hardening direction.

Also applies to: 124-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/library/registry.rs` around lines 107 - 120, The
registry still panics on poisoned mutexes because `Registry::all`,
`Registry::get`, and the other lock sites in
`crates/vibege-scene/src/library/registry.rs` use `.expect("registry lock")`.
Update these methods and the scan/update path to recover from poison by taking
the inner value with `into_inner()` (matching the PR’s lock-hardening approach)
instead of unwrapping, so `games`, `by_name`, and `last_scan` continue operating
after a prior panic.
crates/vibege-config/src/migration.rs (1)

10-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead/fragile version-handling logic.

The post-loop config.version < MIN_SUPPORTED_VERSION check (lines 25-33) is currently unreachable: any version entering the while loop that doesn't match an explicit migration arm (including versions below MIN_SUPPORTED_VERSION) is already caught by the wildcard arm at line 17-21, which resets to defaults and returns immediately. The only way to reach line 25 is config.version >= CONFIG_VERSION, which can never be < MIN_SUPPORTED_VERSION given MIN_SUPPORTED_VERSION <= CONFIG_VERSION. Additionally, when CONFIG_VERSION is bumped beyond 2 in the future, a v1 config would migrate to v2 via the explicit arm, then hit the wildcard arm for 2 (since there's no explicit 2 => ... case yet) and get reset to defaults instead of continuing the migration chain — easy to overlook when adding new versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-config/src/migration.rs` around lines 10 - 33, The version
migration flow in migrate_config is redundant and fragile because the wildcard
branch already resets unsupported versions, making the post-loop
MIN_SUPPORTED_VERSION check unreachable. Remove that dead check and rework the
match in migrate_config so future versions are handled explicitly in a forward
migration chain (for example, add a dedicated arm for each supported config
version instead of relying on v =>), ensuring CONFIG_VERSION bumps do not
accidentally reset already-migrated configs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60d55617-fbc7-4e7b-b2da-c0eb1fc74ac7

📥 Commits

Reviewing files that changed from the base of the PR and between e05266a and b55d2c9.

📒 Files selected for processing (110)
  • Cargo.toml
  • crates/vibege-asset/Cargo.toml
  • crates/vibege-asset/src/cache.rs
  • crates/vibege-asset/src/handle.rs
  • crates/vibege-asset/src/lib.rs
  • crates/vibege-asset/src/loader.rs
  • crates/vibege-asset/src/metadata.rs
  • crates/vibege-asset/src/package.rs
  • crates/vibege-asset/src/statistics.rs
  • crates/vibege-asset/src/types.rs
  • crates/vibege-audio/Cargo.toml
  • crates/vibege-audio/src/engine.rs
  • crates/vibege-audio/src/handle.rs
  • crates/vibege-audio/src/lib.rs
  • crates/vibege-audio/src/mixer.rs
  • crates/vibege-audio/src/sound_cache.rs
  • crates/vibege-config/Cargo.toml
  • crates/vibege-config/src/config/audio.rs
  • crates/vibege-config/src/config/developer.rs
  • crates/vibege-config/src/config/graphics.rs
  • crates/vibege-config/src/config/input.rs
  • crates/vibege-config/src/config/mod.rs
  • crates/vibege-config/src/handle.rs
  • crates/vibege-config/src/lib.rs
  • crates/vibege-config/src/migration.rs
  • crates/vibege-config/src/profile.rs
  • crates/vibege-config/src/validation.rs
  • crates/vibege-config/tests/integration.rs
  • crates/vibege-core/src/diagnostics.rs
  • crates/vibege-core/src/error.rs
  • crates/vibege-core/src/event.rs
  • crates/vibege-core/src/lib.rs
  • crates/vibege-core/src/lifecycle.rs
  • crates/vibege-core/src/metrics.rs
  • crates/vibege-core/src/services.rs
  • crates/vibege-core/src/state_machine.rs
  • crates/vibege-input/Cargo.toml
  • crates/vibege-input/src/action.rs
  • crates/vibege-input/src/context.rs
  • crates/vibege-input/src/gamepad.rs
  • crates/vibege-input/src/lib.rs
  • crates/vibege-input/src/mouse.rs
  • crates/vibege-ipc/Cargo.toml
  • crates/vibege-ipc/src/lib.rs
  • crates/vibege-renderer/Cargo.toml
  • crates/vibege-renderer/src/lib.rs
  • crates/vibege-runtime-app/Cargo.toml
  • crates/vibege-runtime-app/src/main.rs
  • crates/vibege-sandbox/Cargo.toml
  • crates/vibege-sandbox/src/lib.rs
  • crates/vibege-scene/Cargo.toml
  • crates/vibege-scene/src/lib.rs
  • crates/vibege-scene/src/library/collections.rs
  • crates/vibege-scene/src/library/history.rs
  • crates/vibege-scene/src/library/integrity.rs
  • crates/vibege-scene/src/library/manager.rs
  • crates/vibege-scene/src/library/mod.rs
  • crates/vibege-scene/src/library/models.rs
  • crates/vibege-scene/src/library/registry.rs
  • crates/vibege-scene/src/library/search.rs
  • crates/vibege-scene/src/library/updates.rs
  • crates/vibege-scene/src/runtime/context.rs
  • crates/vibege-scene/src/runtime/error.rs
  • crates/vibege-scene/src/runtime/lifecycle.rs
  • crates/vibege-scene/src/runtime/mod.rs
  • crates/vibege-scene/src/runtime/orchestrator.rs
  • crates/vibege-scene/src/runtime/session.rs
  • crates/vibege-scene/src/runtime/state.rs
  • crates/vibege-scene/src/runtime/validator.rs
  • crates/vibege-scene/src/scene/kind.rs
  • crates/vibege-scene/src/scene/manager.rs
  • crates/vibege-scene/src/scene/message.rs
  • crates/vibege-scene/src/scene/mod.rs
  • crates/vibege-scene/src/scene/state.rs
  • crates/vibege-scene/src/scene/tests.rs
  • crates/vibege-scene/src/scenes/error_scene.rs
  • crates/vibege-scene/src/scenes/first_run_scene.rs
  • crates/vibege-scene/src/scenes/game_manager.rs
  • crates/vibege-scene/src/scenes/game_scene.rs
  • crates/vibege-scene/src/scenes/home_scene.rs
  • crates/vibege-scene/src/scenes/library_scene.rs
  • crates/vibege-scene/src/scenes/mod.rs
  • crates/vibege-scene/src/scenes/settings_scene.rs
  • crates/vibege-scene/src/scenes/store_scene.rs
  • crates/vibege-scene/src/store/cache.rs
  • crates/vibege-scene/src/store/discovery.rs
  • crates/vibege-scene/src/store/download.rs
  • crates/vibege-scene/src/store/manager.rs
  • crates/vibege-scene/src/store/metadata.rs
  • crates/vibege-scene/src/store/mod.rs
  • crates/vibege-scene/src/store/models.rs
  • crates/vibege-scene/src/store/search.rs
  • crates/vibege-scene/src/ui_helper.rs
  • crates/vibege-sdk/Cargo.toml
  • crates/vibege-sdk/src/assets.rs
  • crates/vibege-sdk/src/audio.rs
  • crates/vibege-sdk/src/input.rs
  • crates/vibege-sdk/src/lib.rs
  • crates/vibege-sdk/src/render.rs
  • crates/vibege-sdk/src/runtime.rs
  • crates/vibege-sdk/src/storage.rs
  • crates/vibege-sdk/src/util.rs
  • crates/vibege-suspension/Cargo.toml
  • crates/vibege-suspension/src/lib.rs
  • crates/vibege-tray/src/lib.rs
  • crates/vibege-window/Cargo.toml
  • crates/vibege-window/src/display.rs
  • crates/vibege-window/src/dpi.rs
  • crates/vibege-window/src/lib.rs
  • crates/vibege-window/src/overlay.rs
💤 Files with no reviewable changes (2)
  • crates/vibege-ipc/Cargo.toml
  • crates/vibege-input/Cargo.toml

Comment on lines +31 to +65
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VibegeConfig {
/// Schema version. Missing (v1 files) default to 1.
#[serde(default = "version_v1")]
pub version: u32,

/// Overlay configuration (originated in v1).
pub overlay: OverlayConfig,

/// Audio configuration.
pub audio: AudioConfig,

/// General / runtime configuration.
pub general: GeneralConfig,

/// Graphics / display configuration (added in v2).
#[serde(default)]
pub graphics: GraphicsConfig,

/// Input / mouse configuration (added in v2).
#[serde(default)]
pub input: InputConfig,

/// Developer options (added in v2).
#[serde(default)]
pub developer: DeveloperConfig,

/// Active profile name (added in v2).
#[serde(default = "default_profile_name")]
pub active_profile: String,

/// Named profiles (added in v2).
#[serde(default)]
pub profiles: ProfileMap,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does the toml crate (0.8.x) still require non-table struct fields to be serialized before table fields, or has the ValueAfterTable restriction been lifted?

💡 Result:

The ValueAfterTable restriction in the toml crate is a fundamental limitation of the TOML specification itself, which requires that all non-table fields (primitive values) must be emitted before any tables [1][2][3]. Because the toml crate uses Serde for serialization, it follows the order dictated by the Rust struct's field definition or the iteration order of maps [2]. This restriction has not been lifted in the 0.8.x version of the toml crate [4]. If your data structure attempts to serialize a table (a nested struct, map, or array of tables) before a primitive value, the serializer will return a ValueAfterTable error [1][5][2]. To resolve this, you have three primary approaches: 1. Reordering: Manually reorder the fields in your Rust struct so that all simple, non-table fields appear before any fields that serialize as tables [1][6]. 2. Using toml_edit: If reordering is impractical (e.g., when dealing with dynamic maps), the toml_edit crate can be used instead [7][8][4]. It provides a more flexible API that does not enforce the same strict serialization ordering requirements as the standard toml crate [7][8]. 3. Serialization helpers: For maps, you can use the toml::ser::tables_last helper function, which explicitly ensures that table-like entries are serialized only after primitive values [2][9][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target module and related tests first.
git ls-files crates/vibege-config/src/config crates/vibege-config/tests | sed 's#^`#FILE` #'

printf '\n== Outline: crates/vibege-config/src/config/mod.rs ==\n'
ast-grep outline crates/vibege-config/src/config/mod.rs --view expanded || true

printf '\n== Outline: crates/vibege-config/tests/integration.rs ==\n'
ast-grep outline crates/vibege-config/tests/integration.rs --view expanded || true

printf '\n== Relevant field order and Default impl ==\n'
sed -n '1,180p' crates/vibege-config/src/config/mod.rs | cat -n

printf '\n== Integration test relevant section ==\n'
sed -n '1,220p' crates/vibege-config/tests/integration.rs | cat -n

Repository: millsydotdev/vibege-runtime

Length of output: 16851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for TOML save/roundtrip usage and serializer helpers.
rg -n "to_string_pretty|to_string\(|tables_last|ValueAfterTable|save\(" crates/vibege-config -S

Repository: millsydotdev/vibege-runtime

Length of output: 3317


Move active_profile before the table fields

toml::to_string_pretty(&VibegeConfig) will hit ValueAfterTable while active_profile sits after overlay/audio/general/graphics/input/developer. Reorder the scalar fields before any table-typed fields here, and keep Default::default() in the same order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-config/src/config/mod.rs` around lines 31 - 65, The TOML
serialization order in VibegeConfig is causing a ValueAfterTable error because
the scalar active_profile is declared after table fields like overlay, audio,
general, graphics, input, and developer. Reorder VibegeConfig so active_profile
comes before any table-typed fields, and make sure the Default implementation
lists fields in the same order to keep serde and default construction aligned.

Comment on lines +91 to +113
impl Validate for VibegeConfig {
fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
errors.extend(self.overlay.validate().err().unwrap_or_default());
errors.extend(self.audio.validate().err().unwrap_or_default());
errors.extend(self.graphics.validate().err().unwrap_or_default());
errors.extend(self.input.validate().err().unwrap_or_default());
errors.extend(self.developer.validate().err().unwrap_or_default());
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}

fn sanitize(&mut self) {
self.overlay.sanitize();
self.audio.sanitize();
self.graphics.sanitize();
self.input.sanitize();
self.developer.sanitize();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

VibegeConfig::validate()/sanitize() never touch general.

GeneralConfig implements Validate (lines 326-380) with real checks (startup_behavior/performance_mode/theme/backend_url enums and URL prefix), but VibegeConfig::validate() (lines 92-104) and sanitize() (lines 106-112) never call self.general.validate()/self.general.sanitize(). As a result, an invalid general section (e.g. a malformed backend_url or unknown theme) silently passes top-level validation and validate_and_fix(), even though tests/integration.rs exercises these exact failure modes directly on GeneralConfig (e.g. test_general_validate_backend_url, test_general_validate_theme) but never through VibegeConfig::validate(), so the gap isn't caught by the test suite.

🐛 Proposed fix
     fn validate(&self) -> Result<(), Vec<String>> {
         let mut errors = Vec::new();
         errors.extend(self.overlay.validate().err().unwrap_or_default());
         errors.extend(self.audio.validate().err().unwrap_or_default());
+        errors.extend(self.general.validate().err().unwrap_or_default());
         errors.extend(self.graphics.validate().err().unwrap_or_default());
         errors.extend(self.input.validate().err().unwrap_or_default());
         errors.extend(self.developer.validate().err().unwrap_or_default());
         if errors.is_empty() {
             Ok(())
         } else {
             Err(errors)
         }
     }
 
     fn sanitize(&mut self) {
         self.overlay.sanitize();
         self.audio.sanitize();
+        self.general.sanitize();
         self.graphics.sanitize();
         self.input.sanitize();
         self.developer.sanitize();
     }
📝 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
impl Validate for VibegeConfig {
fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
errors.extend(self.overlay.validate().err().unwrap_or_default());
errors.extend(self.audio.validate().err().unwrap_or_default());
errors.extend(self.graphics.validate().err().unwrap_or_default());
errors.extend(self.input.validate().err().unwrap_or_default());
errors.extend(self.developer.validate().err().unwrap_or_default());
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn sanitize(&mut self) {
self.overlay.sanitize();
self.audio.sanitize();
self.graphics.sanitize();
self.input.sanitize();
self.developer.sanitize();
}
}
impl Validate for VibegeConfig {
fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
errors.extend(self.overlay.validate().err().unwrap_or_default());
errors.extend(self.audio.validate().err().unwrap_or_default());
errors.extend(self.general.validate().err().unwrap_or_default());
errors.extend(self.graphics.validate().err().unwrap_or_default());
errors.extend(self.input.validate().err().unwrap_or_default());
errors.extend(self.developer.validate().err().unwrap_or_default());
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn sanitize(&mut self) {
self.overlay.sanitize();
self.audio.sanitize();
self.general.sanitize();
self.graphics.sanitize();
self.input.sanitize();
self.developer.sanitize();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-config/src/config/mod.rs` around lines 91 - 113,
VibegeConfig::validate() and sanitize() are skipping the general section, so
invalid GeneralConfig values can pass top-level checks unnoticed. Update the
VibegeConfig impl in validate() and sanitize() to include self.general alongside
overlay, audio, graphics, input, and developer, using GeneralConfig::validate
and GeneralConfig::sanitize so backend_url, theme, startup_behavior, and
performance_mode are enforced consistently.

event_bus.publish(&vibege_core::RuntimeEvent::ShuttingDown);
.run(move |event, elwt| match event {
Event::WindowEvent { event: we, .. } => {
input.lock().expect("Input lock").handle_window_event(&we);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Recover poisoned input locks instead of panicking.

These .expect("Input lock") calls reintroduce lock-poison panic paths in the runtime loop. Use the same poison-recovery pattern as the rest of this runtime-integrity PR.

Proposed fix
-                input.lock().expect("Input lock").handle_window_event(&we);
+                lock_input(&input).handle_window_event(&we);
...
-                input.lock().expect("Input lock").end_frame();
+                lock_input(&input).end_frame();

Add a helper outside main:

fn lock_input(input: &Mutex<InputManager>) -> std::sync::MutexGuard<'_, InputManager> {
    input.lock().unwrap_or_else(|e| {
        warn!("Input lock poisoned — recovering");
        e.into_inner()
    })
}

Also applies to: 332-332

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-runtime-app/src/main.rs` at line 231, Replace the remaining
Input lock panic paths in main with poison recovery. The
`input.lock().expect("Input lock")` call in the window-event handling path, and
the similar call at the other listed site, should use the same recovery approach
as the rest of the runtime integrity changes. Add a small helper such as
`lock_input` near `main` that returns a `MutexGuard<InputManager>` via
`unwrap_or_else`, logs a warning on poison, and recovers with `into_inner()`,
then use that helper wherever `InputManager` is locked.

Comment on lines +98 to +108
by_name.insert(name.clone(), games.len());
games.push(game);
}
}
}
}

games.sort_by(|a, b| a.name.cmp(&b.name));

*self.games.lock().expect("registry lock") = games.clone();
*self.by_name.lock().expect("registry lock") = by_name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Rebuild by_name after sorting games.

by_name stores pre-sort indices, then games is sorted. If disk iteration returns B, A, get("B") can return A, which can cascade into updating or uninstalling the wrong game.

Proposed fix
-                        by_name.insert(name.clone(), games.len());
                         games.push(game);
                     }
                 }
             }
         }
 
         games.sort_by(|a, b| a.name.cmp(&b.name));
+        by_name = games
+            .iter()
+            .enumerate()
+            .map(|(idx, game)| (game.name.clone(), idx))
+            .collect();
 
         *self.games.lock().expect("registry lock") = games.clone();
📝 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
by_name.insert(name.clone(), games.len());
games.push(game);
}
}
}
}
games.sort_by(|a, b| a.name.cmp(&b.name));
*self.games.lock().expect("registry lock") = games.clone();
*self.by_name.lock().expect("registry lock") = by_name;
games.push(game);
}
}
}
}
games.sort_by(|a, b| a.name.cmp(&b.name));
by_name = games
.iter()
.enumerate()
.map(|(idx, game)| (game.name.clone(), idx))
.collect();
*self.games.lock().expect("registry lock") = games.clone();
*self.by_name.lock().expect("registry lock") = by_name;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/library/registry.rs` around lines 98 - 108, The
registry rebuild logic in `registry.rs` is keeping `by_name` indices from before
`games.sort_by`, so name lookups can point to the wrong entry after sorting.
Update the `LibraryRegistry` rebuild flow so `games` is sorted first, then
reconstruct `by_name` from the final sorted `games` vector using each game’s
`name` and its new position. Make sure the fix is applied in the same rebuild
path that currently updates `self.games` and `self.by_name`.

Comment on lines +91 to +95
/// Validate the package.
pub fn validate(&self, engine_version: &str) -> ValidationReport {
let entry_data = Some(self.ctx.source.as_bytes());
PackageValidator::validate(&self.ctx.manifest, entry_data, &[], engine_version)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Advance the controller to Validated on success.

GameRuntime::load_from_source() calls mount() -> validate() -> initialize(). Because Line 92 only returns a report, the controller stays Mounted, and Line 99 then rejects Mounted -> Initialized against RuntimeState::valid_transitions(). That makes the orchestrated runtime path fail immediately after a passing validation.

Suggested direction
-    pub fn validate(&self, engine_version: &str) -> ValidationReport {
+    pub fn validate(&mut self, engine_version: &str) -> Result<ValidationReport, RuntimeError> {
         let entry_data = Some(self.ctx.source.as_bytes());
-        PackageValidator::validate(&self.ctx.manifest, entry_data, &[], engine_version)
+        let report =
+            PackageValidator::validate(&self.ctx.manifest, entry_data, &[], engine_version);
+        if report.passed {
+            self.transition(RuntimeState::Validated)?;
+        }
+        Ok(report)
     }
📝 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
/// Validate the package.
pub fn validate(&self, engine_version: &str) -> ValidationReport {
let entry_data = Some(self.ctx.source.as_bytes());
PackageValidator::validate(&self.ctx.manifest, entry_data, &[], engine_version)
}
/// Validate the package.
pub fn validate(&mut self, engine_version: &str) -> Result<ValidationReport, RuntimeError> {
let entry_data = Some(self.ctx.source.as_bytes());
let report =
PackageValidator::validate(&self.ctx.manifest, entry_data, &[], engine_version);
if report.passed {
self.transition(RuntimeState::Validated)?;
}
Ok(report)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/runtime/session.rs` around lines 91 - 95, Advance the
session controller to Validated inside validate() after
PackageValidator::validate succeeds, so GameRuntime::load_from_source() can
proceed from Mounted to Initialized without tripping
RuntimeState::valid_transitions(). Update the validate method on the
session/controller path to perform the state transition after a successful
report is produced, keeping the existing validation call and preserving the
returned ValidationReport.

Comment on lines +748 to +775
fn call_scene<F>(
scene: &mut dyn Scene,
f: F,
ctx: &mut SceneContext,
pending: &mut VecDeque<SceneAction>,
error_fallback: &mut Option<Box<dyn Scene>>,
) where
F: FnOnce(&mut dyn Scene, &mut SceneContext) -> SceneResult,
{
match f(scene, ctx) {
Ok(SceneAction::Continue) => {}
Ok(action) => {
pending.push_back(action);
}
Err(e) => {
let id = scene.id();
warn!(?id, error = %e, "Scene lifecycle error");
if let Some(fallback) = error_fallback.take() {
let mut node = SceneNode::new(fallback);
let fid = node.id();
let _ = node.scene.on_create(ctx);
let _ = node.scene.on_enter(ctx);
let _ = node.scene.on_activate(ctx);
info!(?fid, "Error fallback displayed");
// We can't push to overlays here without access to self.
// Instead, queue a PushModal action.
pending.push_back(SceneAction::PushModal(node.scene));
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Stop advancing the transition after a lifecycle error.

call_scene() turns Err into queued actions and returns (), so callers keep running later hooks and still insert the scene even when on_create() failed. In the fallback branch it also runs on_create/on_enter/on_activate immediately, then PushModal runs them again. Propagate failure back to the caller and queue the raw fallback scene without pre-initializing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/scene/manager.rs` around lines 748 - 775, The
call_scene() helper is swallowing lifecycle failures and converting them into
queued actions, which lets callers continue the transition and can double-run
fallback initialization. Update call_scene() and its callers to propagate Err
back so the transition stops immediately after a lifecycle error, and in the
error_fallback branch queue the fallback scene itself with
SceneAction::PushModal without calling on_create, on_enter, or on_activate
beforehand.

Comment on lines +105 to +151
let inp = InputState::new(
&ctx.input,
&[
"up", "down", "enter", "escape", "r", "f", "delete", "c", "u",
],
);

if inp.pressed(4)
/* esc */
{
match &self.view_mode {
ViewMode::CollectionView(_) => {
self.view_mode = ViewMode::Collections;
self.selection = 0;
}
ViewMode::Collections => {
self.view_mode = ViewMode::List;
self.selection = 0;
}
ViewMode::List => {
return Ok(SceneAction::Pop);
}
}
return Ok(SceneAction::Continue);
}
if r {
self.games = scan_games();

if inp.pressed(5)
/* r */
{
self.manager.refresh();
self.game_names = self
.manager
.games()
.into_iter()
.map(|g| g.name.clone())
.collect();
self.selection = 0;
return Ok(SceneAction::Continue);
}

if self.games.is_empty() {
if inp.pressed(9) /* c */ && matches!(self.view_mode, ViewMode::List) {
self.view_mode = ViewMode::Collections;
self.selection = 0;
return Ok(SceneAction::Continue);
}

if up && self.selection > 0 {
self.selection -= 1;
}
if down && self.selection + 1 < self.games.len() {
self.selection += 1;
if inp.pressed(8) /* u */ && matches!(self.view_mode, ViewMode::List) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Fix the shifted input indices before this can delete games accidentally.

The binding order is escape=3, r=4, f=5, delete=6, c=7, u=8, but the handlers use 4/5/6/7/9. As written, pressing c can run the uninstall path, and the collections shortcut is unreachable.

Proposed fix
+        const UP: usize = 0;
+        const DOWN: usize = 1;
+        const ENTER: usize = 2;
+        const ESCAPE: usize = 3;
+        const REFRESH: usize = 4;
+        const FAVORITE: usize = 5;
+        const DELETE: usize = 6;
+        const COLLECTIONS: usize = 7;
+        const UPDATES: usize = 8;
+
-        if inp.pressed(4)
+        if inp.pressed(ESCAPE)
...
-        if inp.pressed(5)
+        if inp.pressed(REFRESH)
...
-        if inp.pressed(9) /* c */ && matches!(self.view_mode, ViewMode::List) {
+        if inp.pressed(COLLECTIONS) && matches!(self.view_mode, ViewMode::List) {
...
-        if inp.pressed(8) /* u */ && matches!(self.view_mode, ViewMode::List) {
+        if inp.pressed(UPDATES) && matches!(self.view_mode, ViewMode::List) {
...
-                if inp.pressed(0) && self.selection > 0 {
+                if inp.pressed(UP) && self.selection > 0 {
...
-                if inp.pressed(1) && self.selection + 1 < collections.len() {
+                if inp.pressed(DOWN) && self.selection + 1 < collections.len() {
...
-                if inp.pressed(2) {
+                if inp.pressed(ENTER) {
...
-                if inp.pressed(6)
+                if inp.pressed(FAVORITE)
...
-                if inp.pressed(7)
+                if inp.pressed(DELETE)

Also applies to: 183-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/scenes/library_scene.rs` around lines 105 - 151, The
input key indices in LibraryScene::update are shifted from the binding order,
causing the wrong actions to fire. Update the inp.pressed(...) checks in the
keyboard handler block so they match the declared keys in InputState::new
(escape, r, f, delete, c, u), especially the shortcuts for collections and
uninstall/delete logic. Use the existing ViewMode branching and SceneAction
return flow, but correct the numeric indices consistently in this handler and
the related section referenced by the comment.

Comment on lines +43 to +52
pub fn next(&self) -> Option<DownloadTask> {
let mut queue = self.queue.lock().expect("queue lock");
let mut active = self.active.lock().expect("active lock");
if active.is_some() {
return None;
}
let task = queue.pop_front()?;
*active = Some(task.clone());
Some(task)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Unify mutex acquisition order before this queue is used concurrently.

next() / clear() lock queue then active, while fail() / cancel() do the reverse. Two threads entering those paths concurrently can deadlock the entire download system. Guard both fields with one mutex or enforce one lock order everywhere.

Also applies to: 60-83, 124-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vibege-scene/src/store/download.rs` around lines 43 - 52, The download
queue logic has inconsistent mutex acquisition order across
`DownloadStore::next`, `clear`, `fail`, and `cancel`, which can deadlock when
accessed concurrently. Fix this by either guarding both `queue` and `active`
with a single mutex or making every path acquire the locks in the same order
everywhere. Update the affected methods in `DownloadStore` so their locking
strategy is identical and safe under concurrent use.

@millsydotdev

Copy link
Copy Markdown
Owner Author

Superseded by PR #2

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant