Cleanup: extract constants, fix typecheck, add ESLint, organize store types
Balance Check / balance-simulation (push) Successful in 37s
Balance Check / multi-run-balance (push) Successful in 13m39s
CI / build-and-push (push) Failing after 19s

- Remove unused initCount state from useAuthGate hook
- Replace magic number with MAX_SAVES_PER_USER constant in saves route
- Extract duplicated EMAIL_REGEX and MIN_PASSWORD_LENGTH in auth routes
- Fix game-simulation typecheck failure by adding DOM lib to tsconfig
- Extract store UI types (ActivePage, InfraNav, etc.) to store/types.ts
- Fix let→const for non-reassigned arrays in servingPipeline
- Fix useless initial assignments in reputationSystem
- Fix ambiguous multiline array access in sanityChecks
- Add minimal ESLint config with typescript-eslint
- Add .planning/ and *.log to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 22:45:32 -04:00
parent 6ea136083a
commit ea3951aa0c
13 changed files with 780 additions and 47 deletions
+2
View File
@@ -10,3 +10,5 @@ balance-report*.json
balance-metrics*.csv balance-metrics*.csv
multirun-summary.csv multirun-summary.csv
multirun-timeseries.csv multirun-timeseries.csv
.planning/
*.log
+9 -6
View File
@@ -7,6 +7,9 @@ import { createToken } from '../lib/jwt';
import { authMiddleware } from '../middleware/auth'; import { authMiddleware } from '../middleware/auth';
import type { AppEnv } from '../types'; import type { AppEnv } from '../types';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const MIN_PASSWORD_LENGTH = 8;
const auth = new Hono<AppEnv>(); const auth = new Hono<AppEnv>();
auth.post('/anonymous', async (c) => { auth.post('/anonymous', async (c) => {
@@ -27,11 +30,11 @@ auth.post('/register', authMiddleware, async (c) => {
inviteCode: string; inviteCode: string;
}>(); }>();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (!email || !EMAIL_REGEX.test(email)) {
return c.json({ error: 'Valid email required' }, 400); return c.json({ error: 'Valid email required' }, 400);
} }
if (!password || password.length < 8) { if (!password || password.length < MIN_PASSWORD_LENGTH) {
return c.json({ error: 'Password must be at least 8 characters' }, 400); return c.json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` }, 400);
} }
if (process.env.REQUIRE_INVITE !== 'false') { if (process.env.REQUIRE_INVITE !== 'false') {
@@ -117,8 +120,8 @@ auth.post('/change-password', authMiddleware, async (c) => {
newPassword: string; newPassword: string;
}>(); }>();
if (!newPassword || newPassword.length < 8) { if (!newPassword || newPassword.length < MIN_PASSWORD_LENGTH) {
return c.json({ error: 'New password must be at least 8 characters' }, 400); return c.json({ error: `New password must be at least ${MIN_PASSWORD_LENGTH} characters` }, 400);
} }
if (!user.mustResetPassword) { if (!user.mustResetPassword) {
@@ -193,7 +196,7 @@ auth.post('/change-email', authMiddleware, async (c) => {
currentPassword: string; currentPassword: string;
}>(); }>();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (!email || !EMAIL_REGEX.test(email)) {
return c.json({ error: 'Valid email required' }, 400); return c.json({ error: 'Valid email required' }, 400);
} }
+1 -1
View File
@@ -25,7 +25,7 @@ savesRouter.get('/', async (c) => {
.from(saves) .from(saves)
.where(eq(saves.userId, userId)) .where(eq(saves.userId, userId))
.orderBy(desc(saves.updatedAt)) .orderBy(desc(saves.updatedAt))
.limit(10); .limit(MAX_SAVES_PER_USER);
return c.json({ saves: userSaves }); return c.json({ saves: userSaves });
}); });
-2
View File
@@ -37,7 +37,6 @@ export function useAuthGate(): AuthGateState {
const [admin, setAdmin] = useState(false); const [admin, setAdmin] = useState(false);
const [cloudSave, setCloudSave] = useState<CloudSaveInfo | null>(null); const [cloudSave, setCloudSave] = useState<CloudSaveInfo | null>(null);
const [hasConflict, setHasConflict] = useState(false); const [hasConflict, setHasConflict] = useState(false);
const [initCount, setInitCount] = useState(0);
const init = useCallback(async () => { const init = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -98,7 +97,6 @@ export function useAuthGate(): AuthGateState {
useState(() => { init(); }); useState(() => { init(); });
const retry = useCallback(() => { const retry = useCallback(() => {
setInitCount(c => c + 1);
init(); init();
}, [init]); }, [init]);
+2 -30
View File
@@ -46,37 +46,9 @@ import {
TECH_TREE, onModelDeployed, TECH_TREE, onModelDeployed,
} from '@token-empire/game-engine'; } from '@token-empire/game-engine';
import { INITIAL_RIVALS } from '@token-empire/game-engine'; import { INITIAL_RIVALS } from '@token-empire/game-engine';
import type { ActivePage, InfraNav, ModelsTab, UIState, GameNotification } from './types';
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models' export type { ActivePage, InfraNavLevel, InfraNav, ModelsTab, UIState, GameNotification } from './types';
| 'market' | 'serving' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'invitations' | 'settings';
export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
export interface InfraNav {
level: InfraNavLevel;
clusterId?: string;
campusId?: string;
datacenterId?: string;
}
type ModelsTab = 'overview' | 'train' | 'models' | 'products';
interface UIState {
activePage: ActivePage;
notifications: GameNotification[];
infraNav: InfraNav;
modelsTab: ModelsTab;
}
export interface GameNotification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'danger';
tick: number;
read: boolean;
action?: { label: string; page?: ActivePage; modelsTab?: ModelsTab };
}
function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> { function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> {
return { return {
+30
View File
@@ -0,0 +1,30 @@
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
| 'market' | 'serving' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'invitations' | 'settings';
export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
export interface InfraNav {
level: InfraNavLevel;
clusterId?: string;
campusId?: string;
datacenterId?: string;
}
export type ModelsTab = 'overview' | 'train' | 'models' | 'products';
export interface UIState {
activePage: ActivePage;
notifications: GameNotification[];
infraNav: InfraNav;
modelsTab: ModelsTab;
}
export interface GameNotification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'danger';
tick: number;
read: boolean;
action?: { label: string; page?: ActivePage; modelsTab?: ModelsTab };
}
+18
View File
@@ -0,0 +1,18 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['**/dist/**', '**/node_modules/**', '**/*.js', '**/*.mjs', '**/drizzle/**'],
},
{
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'no-empty': ['error', { allowEmptyCatch: true }],
'preserve-caught-error': 'off',
},
},
);
+4 -1
View File
@@ -5,7 +5,7 @@
"dev": "turbo dev", "dev": "turbo dev",
"build": "turbo build", "build": "turbo build",
"typecheck": "turbo typecheck", "typecheck": "turbo typecheck",
"lint": "turbo lint", "lint": "eslint .",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"clean": "turbo clean", "clean": "turbo clean",
@@ -13,8 +13,11 @@
"simulate:ci": "pnpm --filter @token-empire/game-simulation simulate:ci" "simulate:ci": "pnpm --filter @token-empire/game-simulation simulate:ci"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.2.1",
"turbo": "^2.5.0", "turbo": "^2.5.0",
"typescript": "^5.8.0", "typescript": "^5.8.0",
"typescript-eslint": "^8.59.1",
"vitest": "^4.1.5" "vitest": "^4.1.5"
}, },
"packageManager": "pnpm@10.33.0", "packageManager": "pnpm@10.33.0",
@@ -75,7 +75,7 @@ interface CachedSlot {
} }
let cachedDeploymentVersion = -1; let cachedDeploymentVersion = -1;
let cachedSlots: CachedSlot[] = []; const cachedSlots: CachedSlot[] = [];
const fleetOutput: ModelServingSlot[] = []; const fleetOutput: ModelServingSlot[] = [];
const mainRemaining = new Map<string, number>(); const mainRemaining = new Map<string, number>();
@@ -83,7 +83,7 @@ const mainUsed = new Map<string, number>();
const entRemaining = new Map<string, number>(); const entRemaining = new Map<string, number>();
const entUsed = new Map<string, number>(); const entUsed = new Map<string, number>();
let cachedUtilization: ModelUtilizationEntry[] = []; const cachedUtilization: ModelUtilizationEntry[] = [];
export function resetFleetCache(): void { export function resetFleetCache(): void {
cachedDeploymentVersion = -1; cachedDeploymentVersion = -1;
@@ -15,7 +15,7 @@ export interface ReputationTickResult {
} }
export function processReputation(state: GameState, researchBonuses?: ResearchBonuses): ReputationState & { _safetyIncident?: boolean } { export function processReputation(state: GameState, researchBonuses?: ResearchBonuses): ReputationState & { _safetyIncident?: boolean } {
let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation; let { safetyRecord, publicPerception } = state.reputation;
let safetyIncident = false; let safetyIncident = false;
if (state.models.bestDeployedSafetyScore > 0) { if (state.models.bestDeployedSafetyScore > 0) {
@@ -39,13 +39,13 @@ export function processReputation(state: GameState, researchBonuses?: ResearchBo
const safetyResearchCount = state.research.completedResearch const safetyResearchCount = state.research.completedResearch
.filter(r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional')).length; .filter(r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional')).length;
const complianceBonus = safetyResearchCount * 8; const complianceBonus = safetyResearchCount * 8;
regulatoryStanding = Math.min(100, Math.max(0, const regulatoryStanding = Math.min(100, Math.max(0,
50 + complianceBonus - regulatoryPressure, 50 + complianceBonus - regulatoryPressure,
)); ));
const talentMorale = Object.values(state.talent.departments) const talentMorale = Object.values(state.talent.departments)
.reduce((sum, d) => sum + d.morale, 0) / 4; .reduce((sum, d) => sum + d.morale, 0) / 4;
employeeSatisfaction = talentMorale * 100; const employeeSatisfaction = talentMorale * 100;
const reputationResearchBonus = researchBonuses?.reputationBonus ?? 0; const reputationResearchBonus = researchBonuses?.reputationBonus ?? 0;
publicPerception = Math.min(100, publicPerception + reputationResearchBonus * PUBLIC_PERCEPTION_GROWTH_RATE); publicPerception = Math.min(100, publicPerception + reputationResearchBonus * PUBLIC_PERCEPTION_GROWTH_RATE);
@@ -55,8 +55,8 @@ export function runSanityChecks(metrics: SimulationMetrics[]): SanityCheckResult
const key = 'reputation-scale-consistency'; const key = 'reputation-scale-consistency';
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
const lowName = ['safetyRecord', 'publicPerception', 'employeeSatisfaction', 'regulatoryStanding'] const reputationFields = ['safetyRecord', 'publicPerception', 'employeeSatisfaction', 'regulatoryStanding'];
[components.indexOf(belowThreshold[0])]; const lowName = reputationFields[components.indexOf(belowThreshold[0])];
violations.push({ violations.push({
tick: m.tick, check: key, tick: m.tick, check: key,
message: `${lowName} = ${belowThreshold[0].toFixed(2)} while others are 10+. Likely a scale mismatch (0-1 vs 0-100)`, message: `${lowName} = ${belowThreshold[0].toFixed(2)} while others are 10+. Likely a scale mismatch (0-1 vs 0-100)`,
+1
View File
@@ -1,6 +1,7 @@
{ {
"extends": "@token-empire/tsconfig/node.json", "extends": "@token-empire/tsconfig/node.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ES2022", "DOM"],
"outDir": "dist", "outDir": "dist",
"rootDir": "src" "rootDir": "src"
}, },
+706
View File
File diff suppressed because it is too large Load Diff