From e5278d57e4b0aff67d2d4c55cd92b0c4057bf79d Mon Sep 17 00:00:00 2001 From: BENZOOgataga Date: Sun, 22 Feb 2026 15:28:45 -0800 Subject: [PATCH] v0.10.0 (and v0.10.1 fixes) (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Standardize postcss on 8.4.49 to address CVE in versions < 8.4.49 (#59) * Initial plan * Fix postcss vulnerability by upgrading to 8.5.6 Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Update postcss override to target 8.5.6 to eliminate dual versions Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Standardize on postcss 8.4.49 to fix CVE and eliminate version conflicts Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Fix early-game UX issues: form placeholders, time visibility, completion feedback (#73) * Initial plan * Fix prefilled form fields, add tick countdown, improve workforce UI explanations Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Add toast notifications for research and production completion Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Fix TypeScript errors in toast notifications Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Add release entry for UX improvements Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Remove prefilled allocation percentages from workforce page initial state Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Fix capacity delta input reset to empty string after submission Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Apply code review feedback: improve accessibility, fix countdown boundary, deduplicate toast recipes Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Update design guidelines documentation with new UX patterns Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * feat: Add infrastructure-based production foundation (Phase 1-2) (#75) * Initial plan * feat(sim): add building infrastructure domain layer with Prisma schema and tests Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * chore: add release entry for building infrastructure phase 1 * feat(sim): integrate building operating costs into tick pipeline Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix(sim): fix stale cash bug and reserved cash invariant in building operating costs - Fetch fresh company data for each building to avoid stale cash values when processing multiple buildings - Use availableCash() to respect reservedCashCents when checking affordability - Update AGENTS.md to reflect new 11-stage tick pipeline - Add tests for reserved cash respect and multi-building scenarios Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * refactor(sim): improve building service types and documentation clarity - Remove `any` return types, let TypeScript infer from Prisma - Clarify which invariants are enforced vs planned in module docs - Add note in tick-engine that production validation doesn't check building status yet (Phase 2) - Update building service docs to separate current vs future features Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix(shared): add new building ledger entry types to FinanceLedgerEntryType - Add BUILDING_OPERATING_COST and BUILDING_ACQUISITION to shared API types - Fixes TypeScript compilation errors in finance controller and service - All 53 tests passing Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix(sim): remove explicit any type in building tests to pass linter - Replace `{} as any` with `{} as Prisma.TransactionClient` - Fixes ESLint error: @typescript-eslint/no-explicit-any - All tests passing, TypeScript compilation successful Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Fix OAuth redirect_uri mismatch between better-auth and provider configuration (#77) * Initial plan * feat: Add nginx proxy rule for OAuth callbacks on web domain Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * docs: Update deployment docs and env example for OAuth callback configuration Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * docs: Add deployment guide for OAuth callback redirect URL fix Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: Address PR review feedback - update release area, nginx comments, and use RFC 5737 IP ranges Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Phase 3: Enforce storage capacity limits and production building requirements (#78) * Initial plan * feat: implement storage capacity system and building validation - Add BASE_STORAGE_CAPACITY_PER_REGION and WAREHOUSE_CAPACITY_PER_SLOT constants - Implement calculateRegionalStorageCapacity function - Implement validateStorageCapacity function to check inventory limits - Integrate storage validation in production completions - Integrate storage validation in market settlement - Integrate storage validation in shipment deliveries - Add validateProductionBuildingAvailable function - Integrate building validation in production job creation - Update tick-engine JSDoc with Phase 3 validation notes - Add comprehensive tests for storage capacity system - Add tests for production building validation Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: remove unused imports in storage.test.ts * fix: resolve storage validation race conditions and bot production - Move storage validation BEFORE inventory mutations in production - Move storage validation BEFORE inventory mutations in market settlement - Only validate production buildings for player companies (bots exempt) - Prevents batch processing race condition where multiple operations in same tick could exceed capacity Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: account for net inventory change in production storage validation - Calculate net inventory change (outputs - inputs consumed) - Only validate storage if net change is positive - Update documentation to accurately reflect validation behavior - Addresses bot reviewer feedback on production capacity logic Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: skip storage validation for self-trades in market settlement - Self-trades (same company, region, and item) have net zero inventory change - Skip storage capacity validation when buyer and seller are the same entity - Prevents false positive rejections when company at capacity trades with itself - Addresses bot reviewer feedback on self-trade edge case Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: add warehouse to market-candles test to ensure storage capacity - Test was failing because buyer company lacked sufficient storage capacity - Added warehouse to buyer company in test setup - Warehouse increases capacity by 500 units (base 1000 + warehouse 500 = 1500 total) - Ensures trade settlement doesn't hit storage limit validation Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: add production building and warehouse to seeded player company - Player company now gets a FACTORY (5 capacity slots) for production jobs - Player company now gets a WAREHOUSE (500 capacity) to handle 1088 units of starting inventory - Total capacity: 1000 (base) + 500 (warehouse) = 1500 units - Removes need for per-test warehouse setup in market-candles test - Fixes all integration tests that rely on player company having buildings Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * refactor: reduce starting inventory and remove free warehouse - Reduced player starting inventory from 1088 to 885 units (fits in base 1000 capacity) - Removed free warehouse to avoid giving players buildings with upkeep costs - Keep FACTORY for production capability (no operating cost in seed) - Starting inventory breakdown: * ironOre: 240 → 200 * coal: 140 → 120 * copperOre: 180 → 150 * water: 200 → 150 * fertilizer: 150 → 120 * bioSubstrate: 160 → 130 * ironIngot: 12 → 10 * copperIngot: 6 → 5 Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * Phase 4 & 5: Buildings Management UI + Preflight Validation + Deterministic Overflow Policy + Stuck Shipment Indicator (#79) * Initial plan * feat(api): add buildings API with preflight validation - Add BuildingsController with endpoints for listing, acquiring, and reactivating buildings - Add preflight validation endpoints for production jobs and buy orders - Add storage and capacity info endpoints - Add building type definitions with costs (Phase 5 balance pass) - Add building-related types to shared package - Register buildings module in app.module.ts Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * feat(web): add buildings API client functions - Add building-related parsers to api-parsers - Add buildings API functions to api client - Add support for listing, acquiring, and reactivating buildings - Add preflight validation API calls - Add storage and capacity info API calls Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * feat(web): add Buildings page and UI components - Add Buildings page with region/category grouping - Add building status display with badges - Add Acquire Building dialog with cost preview - Add reactivate building functionality - Add Storage Meter component for capacity visualization - Add missing UI components (Dialog, Label, Progress) - Install required radix-ui packages Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * chore: add release entry for Phase 4 & 5 implementation * fix: remove unused imports in buildings service * feat(sim): implement deterministic overflow policy for shipments CRITICAL FIX: Shipment delivery now returns to sender when destination storage is full, preventing tick blocking. **Overflow Strategy: Return to Sender** When shipment arrives but destination storage is full: - Shipment marked as DELIVERED (not failed) - Inventory returned to origin region (fromRegionId) - No error thrown (prevents tick advancement failures) - Deterministic behavior - always returns, never partial - Player consequence: wasted logistics fee **Changes:** - packages/sim/src/services/shipments.ts: - deliverDueShipmentsForTick() now returns {deliveredCount, returnedCount} - Catches storage capacity errors and returns to sender - Updated JSDoc with comprehensive overflow policy documentation **Prevents Soft-Lock:** - Tick processing never fails due to player storage mismanagement - Multi-operation storage contention handled deterministically - Same-tick shipments processed in arrival order (ASC) All 64 existing tests passing. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * docs: add comprehensive overflow policy and economic analysis Complete documentation covering: - Deterministic overflow policy (reject at source + return to sender) - Multi-operation storage contention handling - Soft-lock prevention strategy - Economic balance analysis with break-even calculations - Early game viability assessment - Over-expansion risk scenarios - Integration test coverage plan Key Findings: - Current building costs too high for absolute beginners - Recommend adding $25k Workshop tier - MegaFactory dangerous if purchased too early (bankruptcy risk) - Production failure behavior needs design decision (lose inputs vs return) All quality gates passing: - ✅ 64/64 tests - ✅ TypeScript compilation - ✅ ESLint Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * feat: add WORKSHOP building tier and improve overflow policy **Workshop Building Tier ($25k Entry-Level):** - Acquisition: $25,000 (vs $80k for Farm) - Weekly cost: $1,500 (vs $4k for Farm) - Capacity: 1 slot - Purpose: True early-game viability **Overflow Policy Enhancements:** - Origin storage validation SKIPPED for returns (prevents deadlock) - Rationale: Items were already at origin - bounded violation acceptable - Origin may temporarily exceed capacity but self-corrects - Deterministic: Same inputs → same outputs - Idempotent: updateMany prevents double-processing - Atomic: Single transaction, no partial state - No ledger entry on return (fee already paid at creation) **Changes:** - packages/db/prisma/schema.prisma: Add WORKSHOP to BuildingType enum - packages/shared/src/api-types.ts: Add WORKSHOP to type union - packages/sim/src/services/buildings.ts: Include WORKSHOP in production building types - packages/sim/src/services/shipments.ts: Enhanced overflow documentation - apps/api/src/buildings/buildings.service.ts: Add WORKSHOP definition and include in preflight **Quality:** - All 64 tests passing - TypeScript compilation passing - ESLint passing Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: maintain hard storage caps and use deterministic ordering BREAKING CHANGES to overflow policy per reviewer feedback: **1. Hard Storage Caps Maintained:** - Origin capacity NOW VALIDATED on rollback (was bypassed) - No bounded violation - all regions respect capacity limits - If both origin and destination full, delivery FAILS - This is acceptable - player made storage management error **2. Delivery Rollback Semantics:** - Clarified: This is a ROLLBACK, not a logistics return - No new shipment created, no additional travel time - Status: DELIVERED (completed, even if rolled back) - Player penalty: wasted fee + time **3. Deterministic Ordering:** - Changed from createdAt (wall-clock) to tickCreated (deterministic) - Guarantees same order on replay: ORDER BY tickArrives ASC, tickCreated ASC - Location: packages/sim/src/services/shipments.ts:674 **Changes:** - packages/sim/src/services/shipments.ts: - Add origin capacity validation on rollback (line 730-736) - Use tickCreated instead of createdAt for ordering - Comprehensive JSDoc updates explaining rollback vs return - Hard cap invariants maintained everywhere - docs/overflow-policy-and-economics.md: - Updated to reflect rollback semantics - Clarified deterministic ordering with tickCreated - Documented edge case (both regions full) **Rationale:** - Hard caps are invariants - no bypass allowed - Edge case failure acceptable (player must manage both regions) - Simpler than overflow bucket (no quarantine system needed) - Deterministic ordering critical for replay All 64 tests passing. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: ensure tick never fails - retry shipments when both regions full CRITICAL FIX: Tick advancement can never be blocked by storage overflow. **Problem:** Previous implementation threw error if both origin and destination were full, blocking tick advancement. **Solution:** Three-tier delivery fallback: 1. Destination has capacity → Normal delivery 2. Destination full, origin has capacity → Rollback to origin (DELIVERED status) 3. Both regions full → Keep IN_TRANSIT, retry next tick (tick continues) **Changes:** - packages/sim/src/services/shipments.ts: - Moved status update AFTER capacity validation (line 746-756) - Only update to DELIVERED if delivery succeeds - If both regions full: shipment stays IN_TRANSIT, no throw - Comprehensive JSDoc explaining three-tier fallback - Line 733: Catch origin validation error, set shouldDeliver=false - docs/overflow-policy-and-economics.md: - Updated to document three-tier delivery outcomes - Clarified tick never fails guarantee - Documented retry mechanism for double-full scenario **Guarantees:** ✅ Tick advancement NEVER fails due to storage ✅ Hard storage caps maintained (no bypass) ✅ Deterministic retry behavior ✅ Player gets clear feedback (shipment stays IN_TRANSIT until space available) **Edge Case Handling:** - Both regions full → shipment retries automatically next tick - Player must clear space in EITHER region for delivery - No soft-lock possible All 64 tests passing. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * refactor: extract production building types constant and fix type safety Address code review feedback: 1. Extract PRODUCTION_BUILDING_TYPES as shared constant (eliminates duplication) 2. Fix type safety: use `as BuildingType` instead of `as any` in acquire dialog **Changes:** - packages/sim/src/services/buildings.ts: - Add PRODUCTION_BUILDING_TYPES export (line 89-97) - Use constant in getProductionCapacityForCompany (line 513) - Use constant in assertHasActiveProductionBuilding (line 649) - apps/api/src/buildings/buildings.service.ts: - Import PRODUCTION_BUILDING_TYPES (line 19) - Use constant in preflightProductionJob (line 337) - apps/web/src/components/buildings/acquire-building-dialog.tsx: - Import BuildingType from api (line 25) - Fix type assertion: as BuildingType (line 90) **Benefits:** - Single source of truth for production building types - Easier maintenance (add/remove building types in one place) - Better type safety (no `as any` bypass) - Consistent across API and sim layers All 64 tests passing, typecheck and lint passing. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix: add database migration for WORKSHOP building type CRITICAL FIX: Database enum missing WORKSHOP value causing 500 errors. **Problem:** Added WORKSHOP to TypeScript code and Prisma schema, but database enum wasn't updated via migration. **Error:** ``` invalid input value for enum "BuildingType": "WORKSHOP" ``` **Solution:** Create migration to add WORKSHOP to BuildingType enum in PostgreSQL. **Migration:** packages/db/prisma/migrations/20260219133500_add_workshop_building_type/migration.sql - Adds WORKSHOP value to BuildingType enum - Required for integration tests to pass - Safe to apply (enum extension, no data migration needed) Integration tests will pass once migration is applied to test database. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * refactor: apply code review suggestions Address copilot-pull-request-reviewer feedback: 1. Remove trailing blank lines in api-parsers.ts 2. Simplify listBuildings controller - pass query DTO directly 3. Wrap building definitions response for API consistency **Changes:** - apps/web/src/lib/api-parsers.ts: Remove trailing whitespace - apps/api/src/buildings/buildings.controller.ts: - Pass query DTO directly to listBuildings (no redundant object) - Wrap definitions in { definitions: [...] } for consistency **Note:** Bot comment about tickCreated field is incorrect - field exists in schema at line 290. All changes are code quality improvements with no functional impact. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * feat: add stuck shipment UI indicator with storage management link Implement real-time visual feedback for stuck shipments (IN_TRANSIT past ETA). **Changes:** - apps/web/src/components/logistics/logistics-page.tsx: - Added Badge and Link imports - Detect stuck shipments: tickArrives < currentTick - Display warning badge with tooltip on stuck shipments - Add "Manage Storage" button linking to /buildings page - Tooltip: "Waiting for storage capacity in destination or origin region" **UX Improvements:** - Clear visual indicator (amber warning badge) when shipment can't deliver - Actionable "Manage Storage" button for direct navigation to Buildings page - Tooltip explains retry behavior (waiting for capacity) - Prevents player confusion about stuck shipments **Implementation Details:** - Uses existing Badge component with "warning" variant - Conditionally renders badge only when isStuck = true - Adds direct link to Buildings page for capacity management - Maintains existing Cancel button functionality No breaking changes. Pure UX enhancement. Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> * fix(auth): support single-origin sso routing * fix(web): preserve repeated proxy headers * fix(ci): apply migrations before APP_ROLE all startup * fix(web): hide seeded example accounts in admin list * fix(api): allow admins on developer catalog read endpoints * fix(web): accept redacted company cash in parsers * fix(api): support admin research catalog on developer page * fix(web): separate recipe input items across catalog views * fix(web): centralize item quantity labels for recipe outputs * fix(web): resolve unknown item labels in market lists * fix(web): scope market listings to company tradable items * fix(auth): support single-origin sso routing * fix(web): preserve repeated proxy headers * fix(ci): apply migrations before APP_ROLE all startup * fix(web): hide seeded example accounts in admin list * fix(api): allow admins on developer catalog read endpoints * fix(web): accept redacted company cash in parsers * fix(api): support admin research catalog on developer page * fix(web): separate recipe input items across catalog views * fix(web): centralize item quantity labels for recipe outputs * fix(web): resolve unknown item labels in market lists * fix(web): scope market listings to company tradable items * docs(ops): use example domains and RFC5737 IPs in nginx docs * docs(ops): drop API subdomain blocks from nginx sample * feat(web): add ALPHA preview disclaimer to footer version badge * feat(web): show alpha notice on version hover and overview * fix(web): remove hover helper text from version badge * fix(web): remove focus ring box from maintenance overlay * feat(web): link alpha version badge to Discord updates * fix(web): fetch Discord URL via runtime public-links endpoint * fix(docs): add guideline to avoid commits to main branch * feat(web): replace static onboarding tutorial with guided walkthrough * fix(web): clarify overview metrics as world-level * feat(web): begin guided tutorial with active company snapshot * fix(sim): prevent zero-trade stalls from static bot books * fix(api): harden diagnostics missing-items service injection * fix(ci): resolve root typecheck failures * fix(web): wrap search params hooks in suspense * fix(ci): run release workflow only on main * fix: stabilize prisma startup and restore phase 1-5 web UX * fix(db): run Prisma generate without relying on dotenv-cli shell binary * fix(web): parse buildings definitions payload correctly * fix(web): support legacy buildings definitions payload shape * chore(release): cut v0.10. 0 * fix(db): sync static catalog for production deployments * chore(release): cut v0.10.1 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- ...200-auto-sync-static-catalog-on-startup.md | 9 + CHANGELOG.md | 6 + package.json | 3 +- packages/db/src/seed-world.ts | 239 ++++++++++++++++++ scripts/sim-sync-static.ts | 26 ++ scripts/start-container.sh | 29 +++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 .releases/released/v0.10.1/20260222232200-auto-sync-static-catalog-on-startup.md create mode 100644 scripts/sim-sync-static.ts diff --git a/.releases/released/v0.10.1/20260222232200-auto-sync-static-catalog-on-startup.md b/.releases/released/v0.10.1/20260222232200-auto-sync-static-catalog-on-startup.md new file mode 100644 index 00000000..d614d151 --- /dev/null +++ b/.releases/released/v0.10.1/20260222232200-auto-sync-static-catalog-on-startup.md @@ -0,0 +1,9 @@ +--- +type: patch +area: db +summary: Add idempotent static catalog sync and run it on container startup +--- + +- Add `syncStaticCatalog` to upsert items, recipes, recipe inputs, research nodes, unlock links, and prerequisites without resetting world state. +- Ensure missing `CompanyRecipe` links are created for existing companies so newly added recipes become available. +- Add `pnpm sim:sync-static` and wire startup to run catalog sync automatically in `APP_ROLE=all` (or when `CORPSIM_SYNC_STATIC_DATA_ON_START=true`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a60b16..b8e23509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -301,3 +301,9 @@ All notable changes to CorpSim are documented in this file. - [db] Run Prisma generate without relying on dotenv-cli shell binary - [web] Fix buildings definitions API response parsing in web client - [web] Accept legacy object-shaped buildings definitions payloads + +## 0.10.1 - 2026-02-22 + +### What's Changed + +- [db] Add idempotent static catalog sync and run it on container startup diff --git a/package.json b/package.json index 92a529ae..ef267bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corpsim", - "version": "0.10.0", + "version": "0.10.1", "private": true, "workspaces": [ "apps/*", @@ -31,6 +31,7 @@ "sim:advance": "tsx scripts/sim-advance.ts", "sim:reset": "tsx scripts/sim-reset.ts", "sim:seed": "pnpm -C packages/db seed", + "sim:sync-static": "tsx scripts/sim-sync-static.ts", "sim:stats": "tsx scripts/sim-stats.ts", "release:entry": "node scripts/release-entry.mjs", "release:check": "node scripts/check-release-entry.mjs", diff --git a/packages/db/src/seed-world.ts b/packages/db/src/seed-world.ts index d7746f74..b00b070f 100644 --- a/packages/db/src/seed-world.ts +++ b/packages/db/src/seed-world.ts @@ -577,6 +577,245 @@ function isRecipeAutoUnlocked(recipeCode: string): boolean { ); } +function chunkArray(rows: T[], chunkSize: number): T[][] { + if (rows.length === 0) { + return []; + } + + const chunks: T[][] = []; + for (let index = 0; index < rows.length; index += chunkSize) { + chunks.push(rows.slice(index, index + chunkSize)); + } + return chunks; +} + +export interface SyncStaticCatalogResult { + itemsSynced: number; + recipesSynced: number; + researchNodesSynced: number; + prerequisitesSynced: number; + companyRecipeLinksCreated: number; +} + +export async function syncStaticCatalog(prisma: PrismaClient): Promise { + return prisma.$transaction(async (tx) => { + const itemsByKey: Record = {}; + + for (const definition of ITEM_DEFINITIONS) { + const item = await tx.item.upsert({ + where: { code: definition.code }, + update: { name: definition.name }, + create: { + code: definition.code, + name: definition.name + } + }); + itemsByKey[definition.key] = item; + } + + const recipesByKey: Record = {}; + for (const definition of RECIPE_DEFINITIONS) { + const outputItem = itemsByKey[definition.outputItemKey]; + if (!outputItem) { + throw new Error(`seed recipe ${definition.code} references unknown output item key ${definition.outputItemKey}`); + } + + const recipe = await tx.recipe.upsert({ + where: { code: definition.code }, + update: { + name: definition.name, + durationTicks: definition.durationTicks, + outputItemId: outputItem.id, + outputQuantity: definition.outputQuantity + }, + create: { + code: definition.code, + name: definition.name, + durationTicks: definition.durationTicks, + outputItemId: outputItem.id, + outputQuantity: definition.outputQuantity + } + }); + + recipesByKey[definition.key] = recipe; + + const expectedInputItemIds: string[] = []; + for (const input of definition.inputs) { + const inputItem = itemsByKey[input.itemKey]; + if (!inputItem) { + throw new Error(`seed recipe ${definition.code} references unknown input item key ${input.itemKey}`); + } + + expectedInputItemIds.push(inputItem.id); + + await tx.recipeInput.upsert({ + where: { + recipeId_itemId: { + recipeId: recipe.id, + itemId: inputItem.id + } + }, + update: { + quantity: input.quantity + }, + create: { + recipeId: recipe.id, + itemId: inputItem.id, + quantity: input.quantity + } + }); + } + + if (expectedInputItemIds.length === 0) { + await tx.recipeInput.deleteMany({ + where: { + recipeId: recipe.id + } + }); + } else { + await tx.recipeInput.deleteMany({ + where: { + recipeId: recipe.id, + itemId: { + notIn: expectedInputItemIds + } + } + }); + } + } + + const researchNodesByKey: Record = {}; + for (const definition of RESEARCH_DEFINITIONS) { + const node = await tx.researchNode.upsert({ + where: { code: definition.code }, + update: { + name: definition.name, + description: definition.description, + costCashCents: definition.costCashCents, + durationTicks: definition.durationTicks + }, + create: { + code: definition.code, + name: definition.name, + description: definition.description, + costCashCents: definition.costCashCents, + durationTicks: definition.durationTicks + } + }); + + researchNodesByKey[definition.key] = node; + + const expectedUnlockRecipeIds: string[] = []; + for (const recipeKey of definition.unlockRecipeKeys) { + const recipe = recipesByKey[recipeKey]; + if (!recipe) { + throw new Error(`research node ${definition.code} references unknown recipe key ${recipeKey}`); + } + expectedUnlockRecipeIds.push(recipe.id); + await tx.researchNodeUnlockRecipe.upsert({ + where: { + nodeId_recipeId: { + nodeId: node.id, + recipeId: recipe.id + } + }, + update: {}, + create: { + nodeId: node.id, + recipeId: recipe.id + } + }); + } + + if (expectedUnlockRecipeIds.length === 0) { + await tx.researchNodeUnlockRecipe.deleteMany({ + where: { + nodeId: node.id + } + }); + } else { + await tx.researchNodeUnlockRecipe.deleteMany({ + where: { + nodeId: node.id, + recipeId: { + notIn: expectedUnlockRecipeIds + } + } + }); + } + } + + await tx.researchPrerequisite.deleteMany(); + await tx.researchPrerequisite.createMany({ + data: RESEARCH_PREREQUISITES.map((entry) => { + const node = researchNodesByKey[entry.nodeKey]; + const prerequisiteNode = researchNodesByKey[entry.prerequisiteKey]; + if (!node) { + throw new Error(`research prerequisite references unknown node key ${entry.nodeKey}`); + } + if (!prerequisiteNode) { + throw new Error(`research prerequisite references unknown prerequisite key ${entry.prerequisiteKey}`); + } + return { + nodeId: node.id, + prerequisiteNodeId: prerequisiteNode.id + }; + }) + }); + + const allRecipes = Object.values(recipesByKey); + const autoUnlockedRecipeIdSet = new Set( + allRecipes.filter((recipe) => isRecipeAutoUnlocked(recipe.code)).map((recipe) => recipe.id) + ); + + let companyRecipeLinksCreated = 0; + if (allRecipes.length > 0) { + const companies = await tx.company.findMany({ + select: { id: true } + }); + + for (const company of companies) { + const rows = allRecipes.map((recipe) => ({ + companyId: company.id, + recipeId: recipe.id, + isUnlocked: autoUnlockedRecipeIdSet.has(recipe.id) + })); + + for (const batch of chunkArray(rows, 1000)) { + const created = await tx.companyRecipe.createMany({ + data: batch, + skipDuplicates: true + }); + companyRecipeLinksCreated += created.count; + } + } + + const autoUnlockedRecipeIds = Array.from(autoUnlockedRecipeIdSet); + if (autoUnlockedRecipeIds.length > 0) { + await tx.companyRecipe.updateMany({ + where: { + recipeId: { + in: autoUnlockedRecipeIds + }, + isUnlocked: false + }, + data: { + isUnlocked: true + } + }); + } + } + + return { + itemsSynced: ITEM_DEFINITIONS.length, + recipesSynced: RECIPE_DEFINITIONS.length, + researchNodesSynced: RESEARCH_DEFINITIONS.length, + prerequisitesSynced: RESEARCH_PREREQUISITES.length, + companyRecipeLinksCreated + }; + }); +} + export async function seedWorld( prisma: PrismaClient, options: SeedWorldOptions = {} diff --git a/scripts/sim-sync-static.ts b/scripts/sim-sync-static.ts new file mode 100644 index 00000000..7949ec4b --- /dev/null +++ b/scripts/sim-sync-static.ts @@ -0,0 +1,26 @@ +import { createPrismaClient, syncStaticCatalog } from "@corpsim/db"; + +async function main(): Promise { + const prisma = createPrismaClient(); + + try { + const result = await syncStaticCatalog(prisma); + console.log( + [ + "Static catalog sync complete.", + `Items: ${result.itemsSynced}`, + `Recipes: ${result.recipesSynced}`, + `Research nodes: ${result.researchNodesSynced}`, + `Prerequisites: ${result.prerequisitesSynced}`, + `Company recipe links created: ${result.companyRecipeLinksCreated}` + ].join(" ") + ); + } finally { + await prisma.$disconnect(); + } +} + +main().catch((error: unknown) => { + console.error("Static catalog sync failed", error); + process.exitCode = 1; +}); diff --git a/scripts/start-container.sh b/scripts/start-container.sh index 05b58288..599fb48e 100644 --- a/scripts/start-container.sh +++ b/scripts/start-container.sh @@ -36,9 +36,37 @@ apply_migrations() { pnpm exec prisma migrate deploy --schema packages/db/prisma/schema.prisma } +is_truthy() { + local value="${1:-}" + local normalized + normalized="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')" + [[ "$normalized" == "1" || "$normalized" == "true" || "$normalized" == "yes" || "$normalized" == "on" ]] +} + +should_sync_static_catalog() { + local configured="${CORPSIM_SYNC_STATIC_DATA_ON_START:-}" + if [[ -n "$configured" ]]; then + is_truthy "$configured" + return $? + fi + + if [[ "$role" == "all" ]]; then + return 0 + fi + + return 1 +} + +sync_static_catalog() { + if should_sync_static_catalog; then + pnpm sim:sync-static + fi +} + run_all() { # Ensure schema is current before starting long-running processes in single-container mode. apply_migrations + sync_static_catalog pnpm --filter @corpsim/api start & api_pid=$! @@ -59,6 +87,7 @@ run_all() { case "$role" in api) + sync_static_catalog run_api ;; web)