Skip to content

[codex] deterministic decoupling: ui, sim runtime, and persistence boundaries#53

Open
Vannon0911 wants to merge 4 commits intomainfrom
codex/working
Open

[codex] deterministic decoupling: ui, sim runtime, and persistence boundaries#53
Vannon0911 wants to merge 4 commits intomainfrom
codex/working

Conversation

@Vannon0911
Copy link
Copy Markdown
Owner

@Vannon0911 Vannon0911 commented Mar 26, 2026

Summary

This PR completes a deterministic architecture split so the UI can ask for intent while kernel/domain remain the write authority. It removes direct ui -> sim imports, moves simulation runtime helpers into canonical sim runtime modules, and makes browser persistence an explicit platform adapter.

Problem

The codebase had tight coupling between UI, simulation helpers, and runtime utility modules. Browser persistence behavior was implicitly tied to kernel defaults. This made boundaries harder to enforce and increased drift risk for deterministic replay and future seed-synchronized multiplayer.

Root Cause

Layer ownership had grown organically:

  • simulation helper logic lived under src/game/runtime/* while reducer logic in src/game/sim/* imported it,
  • UI read and decision logic still touched gameplay fields directly in several places,
  • kernel persistence included browser-specific behavior by default.

What Changed

  1. Canonical sim runtime layer
  • Added src/game/sim/runtime/* for active-order execution, navigation, infra helpers, and state counters.
  • Updated reducer and win-condition imports to use the canonical sim runtime paths.
  • Kept src/game/runtime/* as compatibility re-export facades.
  1. UI decoupling via viewmodel selectors
  • Added viewmodel selector surfaces for builder reads, tile interaction checks, and UI state context.
  • Reworked ui.input.js and ui.js to consume selector functions instead of direct sim imports and scattered raw state reads.
  1. Kernel/platform persistence split
  • Kernel persistence is now platform-neutral (createNullDriver default path).
  • Browser drivers moved to src/platform/persistence/webDriver.js.
  • App/test/browser callsites now inject web drivers explicitly.
  1. Determinism and boundary guard upgrades
  • Added tests/test-deterministic-patch-chain.mjs to enforce identical reducer/simStep patch chains for same seed + same actions.
  • Extended boundary checks to forbid sim -> game/runtime, forbid direct ui -> sim imports, and prevent browser globals in kernel files.
  • Synced evidence registry and status/architecture docs for the new structure.

User Impact

  • No intended gameplay behavior change.
  • Stronger structural guarantees for deterministic replay and future seed-sync multiplayer.
  • Clearer module ownership and lower coupling for future feature work.

Validation

Executed and passing:

  • npm run llm:pipeline:check
  • npm run gate:blocking (truth, determinism matrix, smoke-e2e gate)
  • node tests/test-runtime-boundaries.mjs
  • node tests/test-whole-repo-dispatch-truth.mjs
  • node tests/test-deterministic-patch-chain.mjs
  • node tests/test-persistence-map-builder-reload.mjs
  • npm run test:quick

Smoke e2e note:

  • tests/test-ui-foundation-e2e.mjs reports SKIPPED playwright_not_installed in this environment, and the smoke gate handles that path as designed.

Commit Structure

This PR is intentionally split into atomic commits:

  1. refactor(sim): move runtime helpers into canonical sim runtime layer
  2. refactor(ui): route ui state reads through viewmodel selectors
  3. refactor(kernel): split web persistence into explicit platform adapter
  4. test(determinism): add patch-chain guard and refresh architecture docs

Open with Devin

Summary by CodeRabbit

  • New Features

    • Active order system: Entities now automatically navigate toward and harvest target tiles with real-time progress tracking and DNA collection.
  • Refactoring

    • Decoupled UI from simulation internals through selector-based state access, improving modularity and maintainability.
    • Consolidated persistence drivers into platform-specific module for cleaner kernel abstraction.
  • Tests

    • Added deterministic replay verification to ensure identical seed/action sequences produce consistent patch chains and signatures.

@Vannon0911 Vannon0911 marked this pull request as ready for review March 30, 2026 20:41
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the codebase to establish canonical module locations for simulation runtime logic and restructure persistence driver architecture. Simulation runtime implementations move from src/game/runtime/ to src/game/sim/runtime/, with compatibility re-export facades retained at original paths. Web-specific persistence drivers migrate to a new src/platform/persistence/webDriver.js module, while the kernel store defaults to a neutral driver. UI layer decoupling introduces viewmodel selector modules that serve as indirection layers instead of direct sim imports.

Changes

Cohort / File(s) Summary
Simulation Runtime Canonical Location
src/game/sim/runtime/infraRuntime.js, src/game/sim/runtime/orderNavigation.js, src/game/sim/runtime/processActiveOrderRuntime.js, src/game/sim/runtime/stateCounts.js
New modules implementing core simulation runtime logic: infrastructure candidate masking, entity navigation/movement via BFS, active order progression (harvest/movement/blocking), and state counting utilities. These become the canonical implementations replacing distributed logic.
Simulation Runtime Compatibility Facades
src/game/runtime/infraRuntime.js, src/game/runtime/orderNavigation.js, src/game/runtime/processActiveOrderRuntime.js, src/game/runtime/stateCounts.js
Converted to re-export-only modules delegating to canonical src/game/sim/runtime/* implementations, maintaining stable export surface while shifting implementation source.
Persistence Driver Reorganization
src/kernel/store/persistence.js, src/platform/persistence/webDriver.js
Kernel store now defaults to createNullDriver() instead of platform-specific logic. Web-specific persistence drivers (createWebDriver, createMetaOnlyWebDriver, getDefaultWebDriver) extracted to new platform-specific module.
Reducer Import Path Updates
src/game/sim/reducer/core.js, src/game/sim/reducer/winConditions.js
Updated import paths to consume runtime helpers from canonical src/game/sim/runtime/* instead of legacy src/game/runtime/* locations.
UI Viewmodel Selector Layer
src/game/viewmodel/uiStateSelectors.js, src/game/viewmodel/tileInteractionSelectors.js, src/game/viewmodel/builderSelectors.js, src/game/viewmodel/builderResources.js
New selector modules providing indirection for UI state reads, tile interaction computation, builder-related queries, and resource constants, decoupling UI from direct sim/runtime imports.
UI Layer Refactoring
src/game/ui/ui.js, src/game/ui/ui.input.js
Replaced direct state field reads and sim imports with selector-based accessors from viewmodel modules. Brush context, grid bounds, and tile interaction logic now routed through selectors.
Store Initialization Updates
src/app/main.js, devtools/demo-live-attest.mjs, tests/test-persistence-map-builder-reload.mjs, tests/test-ui-foundation-e2e.mjs
Updated createStore calls to pass explicit storageDriver option via getDefaultWebDriver() from new platform module.
Determinism & Boundary Tests
tests/test-deterministic-patch-chain.mjs, tests/test-runtime-boundaries.mjs, tests/test-active-order-runtime.mjs, tests/test-persistence-drivers.mjs
New determinism test validates identical patch sequences for same-seed replays. Extended boundary tests enforce sim/runtime and UI/sim separation. Updated existing test imports for relocated modules.
Documentation Updates
docs/ARCHITECTURE.md, docs/STATUS.md, tests/evidence/spec-map.mjs
Documented module consolidation, re-export facades, UI selector architecture, and persistence driver separation. Added regression test entry for determinism verification.

Sequence Diagram

sequenceDiagram
    participant App as App/Test Entry
    participant Kernel as Kernel Store
    participant Driver as Storage Driver
    participant Persist as Persistence Layer

    App->>Kernel: createStore(manifest, {reducer, simStep}, {storageDriver: getDefaultWebDriver()})
    Kernel->>Driver: new driver instance (WebDriver or NullDriver)
    Kernel->>Persist: Initialize persistence module
    Persist->>Driver: Call driver.load()
    Driver->>Persist: Return stored document or null
    Persist->>Kernel: Provide hydrated or initial state
    App->>Kernel: dispatch(actions...)
    Kernel->>Driver: On state changes, call driver.save(doc)
    Driver->>Persist: Write document to localStorage
    Persist->>Driver: Confirm save complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 With runtime bundled up with care,
Now sim knows where to find its lair,
While UI learns to ask politely
Through selectors crafted quite so sightly,
Web drivers tucked in platform's place—
Refactoring with bunny-grade grace 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main architectural refactor: moving simulation runtime logic, decoupling UI, and establishing persistence boundaries.
Description check ✅ Passed The description provides comprehensive coverage of summary, problem, root cause, changes, impact, and validation results.

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

✨ 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 codex/working

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

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (4)
src/platform/persistence/webDriver.js (2)

53-53: Minor: export creates a throwaway driver instance.

createWebDriver().export instantiates a new driver just to borrow its export method. This works but creates an unnecessary closure on each call. Consider extracting the export logic to a shared function.

♻️ Optional: Extract shared export function
+const downloadExport = (doc) => {
+  const blob = new Blob([JSON.stringify(doc, null, 2)], { type: "application/json" });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = "state_export.json";
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  URL.revokeObjectURL(url);
+};
+
 export const createWebDriver = (key = "llm_kernel_state") => ({
   // ...
-  export: (doc) => {
-    const blob = new Blob([JSON.stringify(doc, null, 2)], { type: "application/json" });
-    // ...
-  }
+  export: downloadExport,
 });

 export const createMetaOnlyWebDriver = (key = "llm_kernel_meta_v1") => ({
   // ...
-  export: createWebDriver().export,
+  export: downloadExport,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/persistence/webDriver.js` at line 53, The export property
currently calls createWebDriver().export which constructs a throwaway driver to
borrow its export method; refactor by extracting the export logic into a
standalone function (e.g., sharedExport or webDriverExport) and have both
createWebDriver() and the module export delegate to that function instead of
instantiating a driver; update references to use the new sharedExport function
and remove the createWebDriver().export call so no temporary driver is created.

4-7: Consider adding error handling for malformed JSON.

JSON.parse in both load methods can throw if localStorage contains malformed data. While createStore.js wraps driver.load() in a try-catch (Lines 51-55), adding local error handling with a fallback to null would make the drivers more defensive and self-contained.

🛡️ Optional: Add defensive JSON parsing
 load: () => {
   const raw = localStorage.getItem(key);
   if (raw == null) return null;
-  return JSON.parse(raw);
+  try {
+    return JSON.parse(raw);
+  } catch {
+    return null;
+  }
 },

Also applies to: 26-29

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

In `@src/platform/persistence/webDriver.js` around lines 4 - 7, The load
function(s) in src/platform/persistence/webDriver.js currently call
JSON.parse(raw) which can throw on malformed data; wrap the parse in a try/catch
inside each load implementation (the load arrow functions that read localStorage
via getItem(key)) and return null on parse errors, logging or silently
swallowing as appropriate so the driver is defensive and self-contained (apply
the same change to both load implementations referenced around the earlier and
later blocks).
src/game/ui/ui.js (1)

11-12: Good progress on UI decoupling via viewmodel selectors.

The introduction of selectIsMapBuilder, selectRunPhase, and selectUiMeta selectors enforces cleaner boundaries between UI and sim layers. However, there's some inconsistency: while sync() at Line 124 uses selectIsMapBuilder(current), several methods still derive builder state directly from state?.sim?.runPhase:

  • Line 183: _refreshActionFeedbackView default parameter
  • Line 210: _syncBuilderPhaseUi default parameter
  • Line 275: _syncBuilderHoverOverlay default parameter

Consider updating these default parameters to use selectIsMapBuilder(state) for consistency.

♻️ Optional: Consistent selector usage in default parameters
- _refreshActionFeedbackView(state = this._store?.getState?.(), isBuilder = String(state?.sim?.runPhase || "") === RUN_PHASE.MAP_BUILDER) {
+ _refreshActionFeedbackView(state = this._store?.getState?.(), isBuilder = selectIsMapBuilder(state)) {

- _syncBuilderPhaseUi(state = this._store?.getState?.(), isBuilder = String(state?.sim?.runPhase || "") === RUN_PHASE.MAP_BUILDER) {
+ _syncBuilderPhaseUi(state = this._store?.getState?.(), isBuilder = selectIsMapBuilder(state)) {

- _syncBuilderHoverOverlay(state = this._store?.getState?.(), isBuilder = String(state?.sim?.runPhase || "") === RUN_PHASE.MAP_BUILDER) {
+ _syncBuilderHoverOverlay(state = this._store?.getState?.(), isBuilder = selectIsMapBuilder(state)) {

Also applies to: 124-124, 210-211, 245-245

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

In `@src/game/ui/ui.js` around lines 11 - 12, The default parameter logic in
several UI methods still derives builder state from state?.sim?.runPhase; update
those defaults to use the selector for consistency: replace uses in
_refreshActionFeedbackView, _syncBuilderPhaseUi, and _syncBuilderHoverOverlay so
their default parameter calls selectIsMapBuilder(state) (matching sync() which
uses selectIsMapBuilder(current)); ensure you import/select selectIsMapBuilder
is used and remove any direct runPhase-derived truthiness in those default
params so all builder checks use selectIsMapBuilder(state).
tests/test-persistence-map-builder-reload.mjs (1)

36-36: Use a fresh driver instance for reload simulation.

At Line 36, reusing the same storageDriver object can mask reload bugs if driver internals ever become stateful. Prefer constructing a new driver with the same key for the reload store.

♻️ Suggested change
-  const reloadStore = createStore(manifest, { reducer, simStep: simStepPatch }, { storageDriver });
+  const reloadStorageDriver = createMetaOnlyWebDriver(storageKey);
+  const reloadStore = createStore(manifest, { reducer, simStep: simStepPatch }, { storageDriver: reloadStorageDriver });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test-persistence-map-builder-reload.mjs` at line 36, The test currently
reuses the same storageDriver when creating reloadStore via
createStore(manifest, { reducer, simStep: simStepPatch }, { storageDriver }),
which can hide stateful-driver bugs; change the test to construct a new driver
instance (e.g., create a fresh storage driver with the same key/config) and pass
that new instance to createStore for reloadStore so reloadStore uses a fresh
storageDriver while keeping manifest, reducer, and simStepPatch 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 `@src/game/ui/ui.input.js`:
- Around line 396-398: The code re-reads grid dimensions from state.meta (const
gridW/gridH) which shadows and bypasses the gridW/gridH previously obtained from
selectGridSize(state); fix by removing the direct state access and reuse the
already-destructured gridW and gridH variables in the call to
selectBuilderBrushTiles (ensure the call to selectBuilderBrushTiles(gridW,
gridH, wx, wy, brushSize) uses the existing variables rather than
Number(state?.meta?.gridW) / Number(state?.meta?.gridH)); this keeps the
selector abstraction (selectGridSize) authoritative and avoids shadowing.

---

Nitpick comments:
In `@src/game/ui/ui.js`:
- Around line 11-12: The default parameter logic in several UI methods still
derives builder state from state?.sim?.runPhase; update those defaults to use
the selector for consistency: replace uses in _refreshActionFeedbackView,
_syncBuilderPhaseUi, and _syncBuilderHoverOverlay so their default parameter
calls selectIsMapBuilder(state) (matching sync() which uses
selectIsMapBuilder(current)); ensure you import/select selectIsMapBuilder is
used and remove any direct runPhase-derived truthiness in those default params
so all builder checks use selectIsMapBuilder(state).

In `@src/platform/persistence/webDriver.js`:
- Line 53: The export property currently calls createWebDriver().export which
constructs a throwaway driver to borrow its export method; refactor by
extracting the export logic into a standalone function (e.g., sharedExport or
webDriverExport) and have both createWebDriver() and the module export delegate
to that function instead of instantiating a driver; update references to use the
new sharedExport function and remove the createWebDriver().export call so no
temporary driver is created.
- Around line 4-7: The load function(s) in src/platform/persistence/webDriver.js
currently call JSON.parse(raw) which can throw on malformed data; wrap the parse
in a try/catch inside each load implementation (the load arrow functions that
read localStorage via getItem(key)) and return null on parse errors, logging or
silently swallowing as appropriate so the driver is defensive and self-contained
(apply the same change to both load implementations referenced around the
earlier and later blocks).

In `@tests/test-persistence-map-builder-reload.mjs`:
- Line 36: The test currently reuses the same storageDriver when creating
reloadStore via createStore(manifest, { reducer, simStep: simStepPatch }, {
storageDriver }), which can hide stateful-driver bugs; change the test to
construct a new driver instance (e.g., create a fresh storage driver with the
same key/config) and pass that new instance to createStore for reloadStore so
reloadStore uses a fresh storageDriver while keeping manifest, reducer, and
simStepPatch unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7efe53a3-cda8-4485-b7e2-ae56f8ee1e39

📥 Commits

Reviewing files that changed from the base of the PR and between 10e8a9c and df4b006.

📒 Files selected for processing (30)
  • devtools/demo-live-attest.mjs
  • docs/ARCHITECTURE.md
  • docs/STATUS.md
  • src/app/main.js
  • src/game/runtime/infraRuntime.js
  • src/game/runtime/orderNavigation.js
  • src/game/runtime/processActiveOrderRuntime.js
  • src/game/runtime/stateCounts.js
  • src/game/sim/reducer/core.js
  • src/game/sim/reducer/winConditions.js
  • src/game/sim/runtime/infraRuntime.js
  • src/game/sim/runtime/orderNavigation.js
  • src/game/sim/runtime/processActiveOrderRuntime.js
  • src/game/sim/runtime/stateCounts.js
  • src/game/ui/ui.input.js
  • src/game/ui/ui.js
  • src/game/viewmodel/builderResources.js
  • src/game/viewmodel/builderSelectors.js
  • src/game/viewmodel/tileInteractionSelectors.js
  • src/game/viewmodel/uiStateSelectors.js
  • src/kernel/store/createStore.js
  • src/kernel/store/persistence.js
  • src/platform/persistence/webDriver.js
  • tests/evidence/spec-map.mjs
  • tests/test-active-order-runtime.mjs
  • tests/test-deterministic-patch-chain.mjs
  • tests/test-persistence-drivers.mjs
  • tests/test-persistence-map-builder-reload.mjs
  • tests/test-runtime-boundaries.mjs
  • tests/test-ui-foundation-e2e.mjs

Comment on lines +396 to +398
const gridW = Number(state?.meta?.gridW || 16) | 0;
const gridH = Number(state?.meta?.gridH || 16) | 0;
const tiles = selectBuilderBrushTiles(gridW, gridH, wx, wy, brushSize);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent grid size access shadows selector values.

Lines 396-398 directly read state?.meta?.gridW/gridH and shadow the gridW/gridH already destructured from selectGridSize(state) on line 264. This bypasses the selector abstraction and could cause subtle inconsistencies if the selector logic diverges from raw state access.

🔧 Proposed fix: reuse existing gridW/gridH
       if (isBuilderBrushMode(mode)) {
         const brushSize = this._builderBrushSize || 1;
-        const gridW = Number(state?.meta?.gridW || 16) | 0;
-        const gridH = Number(state?.meta?.gridH || 16) | 0;
         const tiles = selectBuilderBrushTiles(gridW, gridH, wx, wy, brushSize);
📝 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
const gridW = Number(state?.meta?.gridW || 16) | 0;
const gridH = Number(state?.meta?.gridH || 16) | 0;
const tiles = selectBuilderBrushTiles(gridW, gridH, wx, wy, brushSize);
const tiles = selectBuilderBrushTiles(gridW, gridH, wx, wy, brushSize);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/game/ui/ui.input.js` around lines 396 - 398, The code re-reads grid
dimensions from state.meta (const gridW/gridH) which shadows and bypasses the
gridW/gridH previously obtained from selectGridSize(state); fix by removing the
direct state access and reuse the already-destructured gridW and gridH variables
in the call to selectBuilderBrushTiles (ensure the call to
selectBuilderBrushTiles(gridW, gridH, wx, wy, brushSize) uses the existing
variables rather than Number(state?.meta?.gridW) / Number(state?.meta?.gridH));
this keeps the selector abstraction (selectGridSize) authoritative and avoids
shadowing.

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