025f627297
阶段一新增受 migration operator 保护的旧玩法表清空 procedure,暂不删除表定义。\n\n固定 63 张旧表,支持 dry-run 与事务 apply;生成 bindings;更新契约和决策记录;保留阶段二删表 TODO。\n\n已完成 cargo check、migration 测试、bindings/schema/encoding/diff 检查;本地 dry-run 返回 63 张表且全为 0 行,未执行 apply。 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/251
222 lines
7.3 KiB
JavaScript
222 lines
7.3 KiB
JavaScript
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/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/editor_project_storage.rs',
|
|
snippet:
|
|
'require_editor_generation_runtime_service_identity(ctx, ctx.sender())',
|
|
reason:
|
|
'procedure runtime writer 鉴权必须显式传 caller,兼容 TxContext sender 为 Identity::ZERO 的 SpacetimeDB 2.6.0',
|
|
},
|
|
];
|
|
|
|
const procedureResultFiles = [];
|
|
|
|
const mapperCompatibilityFiles = [
|
|
'server-rs/crates/spacetime-client/src/active/mapper.rs',
|
|
'server-rs/crates/spacetime-client/src/active.rs',
|
|
];
|
|
|
|
const bigFishRuntimeFiles = [];
|
|
|
|
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<String>/gu;
|
|
|
|
const failures = [];
|
|
|
|
for (const rule of forbiddenSnippets) {
|
|
if (
|
|
rule.reason.startsWith('profile_save_archive') ||
|
|
rule.reason.startsWith('profile_played_world')
|
|
) {
|
|
continue;
|
|
}
|
|
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 检查通过。');
|