Skip to content

Integrate alpha stabilization release candidate - #124

Merged
BrandDead merged 9 commits into
main-tL2525from
integration/alpha-stabilization
Sep 2, 2026
Merged

Integrate alpha stabilization release candidate#124
BrandDead merged 9 commits into
main-tL2525from
integration/alpha-stabilization

Conversation

@BrandDead

@BrandDead BrandDead commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Purpose

This is the single integration path for the three reviewed, non-overlapping alpha-stabilization pull requests. It was assembled in an isolated worktree and is intentionally not merged into main-tL2525.

Integrated source work

All three heads merged locally without textual conflicts. The integration adds no new product scope beyond those source pull requests.

Combined validation

  • npm run validate — 50 test files / 716 tests passed; existing lint warnings remained warnings only; asset audit passed with zero errors; five packages validated.
  • Standard production build — passed.
  • VITE_DEMO_MODE=1 production build — passed.
  • Backend pytest -q — 42 tests passed.
  • Migration static safety review — additive only; no destructive SQL patterns.
  • Browser smoke (non-demo) — missing configuration presents the styled recovery screen, not a blank frame.
  • Browser smoke (demo) — DEALT route initializes and completes a deterministic result transition; map/War Room and target-to-loadout prerequisite flow initialize.

Guardrails

  • The pure deterministic combat domain remains untouched.
  • No second engine, store, payment system, broad multiplayer system, or asset-package redesign is introduced.
  • 005_authoritative_world_foundation.sql is not applied by this PR; database and Edge Function deployment remain separate, reviewed release gates.
  • Source PR Add authoritative Ghost Crew world foundation #123 currently has a pending Cursor Bugbot Autofix status. This integration PR should not be merged until that status and this PRs own required checks complete successfully or are explicitly waived.

Merge policy

If this PR is approved and all checks pass, merge this integration PR only. Then close #121, #122, and #123 as superseded. Do not merge the three source PRs individually afterwards.

See docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.md for evidence and release gates.


Note

Medium Risk
Adds security-definer RPCs, block metadata/heat updates from encounter receipts, and cron-gated world ticks; changes are additive but affect persistence reconciliation and require correct migration/secret deployment before production.

Overview
This PR is the single merge path for three reviewed scopes: a CC0 production character/PBR art package (#121), non-demo startup recovery when Supabase/env is missing (#122), and an additive authoritative Ghost Crew / encounter-receipt backend with matching client hooks (#123).

Art (#121): Docs and ASSETS.md now record shipped Quaternius CC0 base + animation clips as universal-male.v1.glb with license/manifest; the Modern Ops path loads the strict package before scene readiness and falls back on failure (per integration validation).

World (#123): New migrations add ghost_crews, world_ticks, world_events, encounter_results, and claimed_block_dna, plus idempotent RPCs apply_ghost_world_tick (service-only) and commit_encounter_result (authenticated, block-owner). Migration 006 bounds encounter deltas and adds persist_player_block_projection so debounced block sync cannot overwrite a newer encounter receipt. Edge functions combat/commit-result and world-tick-ghost (cron secret) call those RPCs. The app hydrates ghost crews via useGhostCrewSync, disables local browser ghost ticks when authenticated, posts completed encounters from BlockModeView, and routes UUID block saves through the atomic projection RPC.

Process: docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.md documents combined test/build gates; DB and edge deployment remain out-of-band until reviewed.

Reviewed by Cursor Bugbot for commit 8765f9a. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
slide Ready Ready Preview Sep 2, 2026 8:33pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T19:58:58.860304Z 065da0d PR opened
🔒 Security Review Completed 2026-09-02T19:57:29.563489Z 065da0d PR opened
ℹ️ About Codex in GitHub

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

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

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 065da0d237

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +293 to +295
v_heat_delta := COALESCE((p_payload->>'heatDelta')::integer, 0);
v_morale_delta := COALESCE((p_payload->>'moraleDelta')::integer, 0);
v_pending_income_delta := COALESCE((p_payload->>'pendingIncomeDelta')::integer, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate encounter rewards inside the RPC

Any authenticated player who owns a block can invoke this SECURITY DEFINER RPC directly with arbitrary integer deltas and a fresh caller-chosen result key, bypassing the Edge Function's Zod schema because the migration grants authenticated direct execution and the frontend itself uses .rpc. Repeated calls can therefore mint unbounded pendingIncome, alter morale, and create apparently valid encounter receipts/events without completing combat; verify a server-issued encounter result and enforce its permitted deltas inside the RPC, or remove direct authenticated execution.

Useful? React with 👍 / 👎.

Comment thread frontend/src/App.tsx
const { raidBlockId, clearRaid } = useRaidCheck();
useBlockSync(!IS_DEMO_MODE);
useEmpireHydration(Boolean(authUser) && authChecked && !IS_DEMO_MODE);
useGhostCrewSync(authUser?.id ?? null, Boolean(authUser) && authChecked && !IS_DEMO_MODE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Disable local ghost ticks after server hydration

On the authenticated production path this hydrates server state, but App still unconditionally starts useGhostTick() below, so every browser independently mutates those crews and claims turf every 30 seconds without writing the results back to the authoritative tables. Multiple clients consequently see different rival states, and a reload replaces the local moves and grudges with the older server snapshot; local ticking should run only in demo/offline fallback mode once authoritative hydration succeeds.

Useful? React with 👍 / 👎.

Comment thread frontend/src/stores/ghostCrewStore.ts Outdated
Comment on lines +193 to +196
set(
(state) => ({
crews: Object.keys(indexedCrews).length > 0 ? indexedCrews : state.crews,
feed: feed.length > 0 ? feed.slice(0, FEED_LIMIT) : state.feed,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebuild block-store turf when hydrating crews

When an authoritative crew has non-empty ownedBlockIds, this replacement updates only the ghost store. The map, recon sheet, and attack flow read NPC territory from useBlockStore, while loadPlayerBlocks loads only the current player's blocks; unlike runTick, this path never calls buildGhostBlock/upsertBlock. A fresh authenticated device therefore cannot see or attack any durable rival turf until unrelated local ticks create new blocks, so hydration must also project or fetch the authoritative owned blocks into the block store.

Useful? React with 👍 / 👎.

Comment thread frontend/src/stores/ghostCrewStore.ts Outdated
set(
(state) => ({
crews: Object.keys(indexedCrews).length > 0 ? indexedCrews : state.crews,
feed: feed.length > 0 ? feed.slice(0, FEED_LIMIT) : state.feed,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear account-scoped feed when the server returns none

When an authenticated account has no visible world_events, this branch preserves the globally persisted local feed instead of accepting the empty authoritative result. Because slide-ghost-crews is not scoped or cleared during the account-switch reset, signing into a second account on the same browser can expose the previous account's recipient-only encounter summaries and rival activity; replace the feed even when it is empty, or scope the persisted store by profile.

Useful? React with 👍 / 👎.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Encounter RPC allows unbounded income
    • Added validation bounds (±5 heat, ±100 morale, ±1M pendingIncome) to commit_encounter_result RPC to match edge function Zod schema and prevent unbounded client-supplied deltas.
  • ✅ Fixed: Ghost hydration wipes local ticks
    • Modified replaceAuthoritativeState to merge crews preserving local progress and disabled useGhostTick when authenticated to prevent local/server state divergence.
  • ✅ Fixed: Block persist clobbers encounter effects
    • Updated persistBlock to conditionally update only when lastEncounterResultKey hasn't changed, preventing stale persists from overwriting encounter-committed deltas.

Create PR

Or push these changes by commenting:

@cursor push 065da0d237

You can send follow-ups to the cloud agent here.

Comment thread backend/supabase/migrations/005_authoritative_world_foundation.sql
Comment thread frontend/src/stores/ghostCrewStore.ts
Comment thread frontend/src/services/blockPersistence.service.ts Outdated

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Local ghost state shadows server hydration
    • Removed timestamp comparison in replaceAuthoritativeState to always prefer server data, ensuring authenticated sessions follow the server-led world timeline.
  • ✅ Fixed: Encounter key mismatch blocks persistence
    • Restored appliedEncounterResultKeys from server metadata in loadPlayerBlocks, allowing persist_player_block_projection to validate encounter keys correctly.

Create PR

Or push these changes by commenting:

@cursor push 60918b28a3
Preview (60918b28a3)
diff --git a/frontend/src/services/blockPersistence.service.ts b/frontend/src/services/blockPersistence.service.ts
--- a/frontend/src/services/blockPersistence.service.ts
+++ b/frontend/src/services/blockPersistence.service.ts
@@ -150,22 +150,26 @@
     return [];
   }
 
-  return data.map((row: any) => ({
-    id: row.id,
-    address: row.address,
-    owner: 'player' as const,
-    heat: Math.round((row.block_heat ?? 0) / 20),
-    incomePerTick: row.base_income ?? 0,
-    morale: row.metadata?.morale ?? 80,
-    pendingIncome: row.metadata?.pendingIncome ?? 0,
-    viewMode: row.metadata?.viewMode ?? 'topdown',
-    streetBackdropUrl: row.metadata?.streetBackdropUrl,
-    topdownBgUrl: row.metadata?.topdownBgUrl,
-    dnaId: row.metadata?.dnaId,
-    incomeMultiplier: row.metadata?.incomeMultiplier,
-    heatDecayMultiplier: row.metadata?.heatDecayMultiplier,
-    maxMembers: row.metadata?.maxMembers,
-  }));
+  return data.map((row: any) => {
+    const lastEncounterResultKey = row.metadata?.lastEncounterResultKey;
+    return {
+      id: row.id,
+      address: row.address,
+      owner: 'player' as const,
+      heat: Math.round((row.block_heat ?? 0) / 20),
+      incomePerTick: row.base_income ?? 0,
+      morale: row.metadata?.morale ?? 80,
+      pendingIncome: row.metadata?.pendingIncome ?? 0,
+      viewMode: row.metadata?.viewMode ?? 'topdown',
+      streetBackdropUrl: row.metadata?.streetBackdropUrl,
+      topdownBgUrl: row.metadata?.topdownBgUrl,
+      dnaId: row.metadata?.dnaId,
+      incomeMultiplier: row.metadata?.incomeMultiplier,
+      heatDecayMultiplier: row.metadata?.heatDecayMultiplier,
+      maxMembers: row.metadata?.maxMembers,
+      appliedEncounterResultKeys: lastEncounterResultKey ? [lastEncounterResultKey] : undefined,
+    };
+  });
 }
 
 // ─── Placement CRUD ──────────────────────────────────────────

diff --git a/frontend/src/stores/ghostCrewStore.ts b/frontend/src/stores/ghostCrewStore.ts
--- a/frontend/src/stores/ghostCrewStore.ts
+++ b/frontend/src/stores/ghostCrewStore.ts
@@ -195,13 +195,10 @@
 
               const nextCrews = { ...state.crews };
               for (const remoteCrew of crews) {
-                const localCrew = state.crews[remoteCrew.id];
-                // Browser state may have progressed while the initial fetch was
-                // in flight. Keep the newest complete crew record; authenticated
-                // sessions disable local ticking once hydration is active.
-                const localTick = localCrew ? Date.parse(localCrew.lastTickAt) : Number.NEGATIVE_INFINITY;
-                const remoteTick = Date.parse(remoteCrew.lastTickAt);
-                nextCrews[remoteCrew.id] = localCrew && localTick > remoteTick ? localCrew : remoteCrew;
+                // In authenticated sessions, the server is authoritative.
+                // Always overlay remote crew data to ensure production sessions
+                // follow the server-led world timeline rather than stale local seeds.
+                nextCrews[remoteCrew.id] = remoteCrew;
               }
 
               const seen = new Set<string>();

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.

return true;
}).slice(0, FEED_LIMIT);

return { crews: nextCrews, feed: mergedFeed };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Local ghost state shadows server hydration

High Severity

replaceAuthoritativeState keeps a local crew whenever its lastTickAt is newer than the server snapshot. Seeded and persisted local timestamps are almost always later than the static database seed, so authenticated hydration never overlays durable Ghost Crew state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.

if (!projectionError) {
if (data?.applied === false) {
console.warn('[BlockPersistence] Skipped stale block projection after a newer encounter result.');
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Encounter key mismatch blocks persistence

High Severity

The new projection RPC rejects writes when p_client_result_key does not match lastEncounterResultKey, while persistBlock only sends the in-memory appliedEncounterResultKeys tail and returns without retry on applied: false. After an encounter those keys can diverge, and later block autosaves never persist.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.

@BrandDead
BrandDead merged commit 5cb2856 into main-tL2525 Sep 2, 2026
6 checks passed
@BrandDead
BrandDead deleted the integration/alpha-stabilization branch September 2, 2026 20:39
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