import fs from 'node:fs'; import path from 'node:path'; const repoRoot = process.cwd(); function readUtf8(relativePath) { const absolute = path.join(repoRoot, relativePath); if (!fs.existsSync(absolute)) { failures.push(`${relativePath}: 文件不存在,无法执行 SpacetimeDB runtime access 检查`); return null; } return fs.readFileSync(absolute, 'utf8'); } const forbiddenSnippets = [ { file: 'server-rs/crates/spacetime-module/src/puzzle.rs', snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.owner_user_id == input.owner_user_id)', reason: 'puzzle_work_profile 已有 by_puzzle_work_owner_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/puzzle.rs', snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.publication_status == PuzzlePublicationStatus::Published)', reason: 'puzzle_work_profile 已有 by_puzzle_work_publication_status 索引', }, { file: 'server-rs/crates/spacetime-module/src/puzzle.rs', snippet: '.puzzle_leaderboard_entry()\n .iter()\n .filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)', reason: 'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引', }, { file: 'server-rs/crates/spacetime-module/src/match3d/mod.rs', snippet: '.match3d_work_profile()\n .iter()\n .filter(|row| {', reason: 'match3d_work_profile 已有 owner/status 索引,列表不应整表过滤', }, { file: 'server-rs/crates/spacetime-module/src/visual_novel.rs', snippet: '.visual_novel_work_profile()\n .iter()\n .filter(|row| {', reason: 'visual_novel_work_profile 已有 owner/status 索引,列表不应整表过滤', }, { file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs', snippet: '.asset_object()\n .iter()\n .find(|row| row.bucket == input.bucket && row.object_key == input.object_key)', reason: 'asset_object 已有 by_bucket_object_key 索引', }, { file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs', snippet: '.asset_object()\n .iter()\n .filter(|row| row.asset_kind == asset_kind)', reason: 'asset_object 已有 asset_kind 索引', }, { file: 'server-rs/crates/spacetime-module/src/ai/stages.rs', snippet: '.ai_task_stage()\n .iter()\n .filter(|row| row.task_id == task_id)', reason: 'ai_task_stage 已有 by_ai_task_stage_task_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/ai/stages.rs', snippet: '.ai_text_chunk()\n .iter()\n .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)', reason: 'ai_text_chunk 已有 by_ai_text_chunk_task_id / by_ai_text_chunk_task_stage_sequence 索引', }, { file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs', snippet: '.ai_task_stage()\n .iter()\n .filter(|stage| stage.task_id == row.task_id)', reason: 'ai_task_stage 快照组装应使用 by_ai_task_stage_task_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs', snippet: '.ai_result_reference()\n .iter()\n .filter(|reference| reference.task_id == row.task_id)', reason: 'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.profile_save_archive()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', reason: 'profile_save_archive 已有 by_profile_save_archive_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.profile_played_world()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', reason: 'profile_played_world 已有 by_profile_played_world_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.profile_wallet_ledger()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', reason: 'profile_wallet_ledger 已有 by_profile_wallet_ledger_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.profile_referral_relation()\n .iter()\n .filter(|row| row.inviter_user_id == user_id)', reason: 'profile_referral_relation 已有 by_profile_referral_inviter_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.profile_recharge_order()\n .iter()\n .filter(|row| row.user_id == user_id)', reason: 'profile_recharge_order 已有 by_profile_recharge_order_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', snippet: '.tracking_daily_stat()\n .iter()\n .filter(|row| {', reason: 'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤', }, { file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs', snippet: '.custom_world_profile()\n .iter()\n .find(|row| {', reason: 'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引', }, { file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs', snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {', reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引', }, ]; const procedureResultFiles = [ 'server-rs/crates/module-puzzle/src/application.rs', 'server-rs/crates/module-big-fish/src/domain.rs', 'server-rs/crates/spacetime-module/src/match3d/types.rs', 'server-rs/crates/spacetime-module/src/square_hole/types.rs', 'server-rs/crates/spacetime-module/src/visual_novel.rs', 'server-rs/crates/spacetime-module/src/bark_battle/types.rs', ]; const mapperCompatibilityFiles = [ 'server-rs/crates/spacetime-client/src/mapper.rs', 'server-rs/crates/spacetime-client/src/lib.rs', ]; const bigFishRuntimeFiles = [ 'server-rs/crates/module-big-fish/src/commands.rs', 'server-rs/crates/spacetime-module/src/big_fish/runtime.rs', 'server-rs/crates/spacetime-module/src/big_fish/session.rs', ]; const legacyMapperPatterns = [ { pattern: /\b[A-Za-z0-9_]*JsonRecord\b/u, reason: 'spacetime-client mapper 不应保留旧 ProcedureResult JSON 兼容 Record', }, { pattern: /\bCompatibleBigFish[A-Za-z0-9_]*\b/u, reason: 'spacetime-client mapper 不应保留 BigFish 旧 JSON 兼容结构', }, { pattern: /\bmap_[A-Za-z0-9_]*_json\b/u, reason: 'spacetime-client mapper 不应再通过 map_*_json 反序列化 procedure payload', }, { pattern: /serde_json::from_str::<[A-Za-z0-9_:]*JsonRecord/u, reason: 'spacetime-client mapper 不应把 procedure result 再反序列化为 JsonRecord', }, { pattern: /\b(?:items|run|work|session|event|feedback)_json:\s*Some\(/u, reason: 'mapper 测试与兼容路径不应再构造旧 procedure JSON 字符串字段', }, ]; const typedProcedurePayloadFieldPattern = /\b(?:row|session|work|item|items|run|event|feedback)_json:\s*Option/gu; const failures = []; for (const rule of forbiddenSnippets) { const content = readUtf8(rule.file); if (content === null) { continue; } if (content.includes(rule.snippet)) { failures.push(`${rule.file}: ${rule.reason}`); } } for (const file of procedureResultFiles) { const content = readUtf8(file); if (content === null) { continue; } const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? []; for (const block of resultBlocks) { const jsonFields = block.match(typedProcedurePayloadFieldPattern); if (jsonFields?.length) { const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult'; failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`); } } } for (const file of mapperCompatibilityFiles) { const content = readUtf8(file); if (content === null) { continue; } for (const rule of legacyMapperPatterns) { if (rule.pattern.test(content)) { failures.push(`${file}: ${rule.reason}`); } } } for (const file of bigFishRuntimeFiles) { const content = readUtf8(file); if (content === null) { continue; } const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? []; for (const block of resultBlocks) { const jsonFields = block.match(typedProcedurePayloadFieldPattern); if (jsonFields?.length) { const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult'; failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`); } } } if (failures.length > 0) { console.error('SpacetimeDB runtime access 检查失败:'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('SpacetimeDB runtime access 检查通过。');