合并最新 master 分支

同步 SpacetimeDB schema guard 基线扫描修复。

同步相关测试、排障记忆与融合方案文档。
This commit is contained in:
2026-07-21 06:42:29 +00:00
4 changed files with 122 additions and 25 deletions
@@ -3241,6 +3241,14 @@
- 处理:把仍在用的公共账号 / 钱包 / 设置能力迁到明确的现役 client 与 presentation modelVite `pre` transform 对退役模块真实路径直接失败,ESLint 在现役源上增加 restricted imports。每次恢复公共 UI 后用 `tsc --listFilesOnly` 和全新浏览器 context 复核,不能用已有 HMR 会话判绿。
- 关联:`vite.config.ts``.eslintrc.cjs``src/services/platform-entry/``docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`
## SpacetimeDB schema guard 的基线不能递归扫描保留源码
- 现象:旧业务按“数据壳保留、业务实现退役”落地后,`check:spacetime-schema` 报几十个 `legacy_schema` 与原路径 accessor 重复;同一提交对自身比较也失败,但 `cargo` 实际可以正常编译 module。
- 原因:当前工作树按 `Cargo.toml [lib].path` 的 crate root 可达模块扫描,基线提交却通过 `git ls-tree -r` 扫描整个 `spacetime-module/src`。原 `src/lib.rs` 和旧业务源码只供追溯、不进入 active crate,但基线全目录扫描仍会把它们与 `#[path]` 引入的历史数据壳同时解析。
- 处理:current 与 base 必须各自读取所在快照的 Cargo manifest,并沿各自 `mod` / `#[path]` 图扫描;base 文件存在性和内容从该 Git tree 读取,不能复用当前工作树。不要忽略 `legacy_schema`、删除历史源码或吞掉 base duplicate,因为历史数据壳正是正式 schema,真实可达重复仍须失败。
- 验证:回归测试同时覆盖“不可达旧源码同 accessor 不报错”和“两个可达模块同 accessor 仍失败”;再运行 `npm run check:spacetime-schema -- --base-ref HEAD`,确认 self-base 按当前 136 张表通过。
- 关联:`scripts/check-spacetime-schema-guard.mjs``scripts/check-spacetime-schema-guard.test.ts``server-rs/crates/spacetime-module/Cargo.toml``docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`
## VectorEngine 请求超时不能脱离 worker 绝对预算(2026-07-20
- 现象:VectorEngine 单次请求超时大于 worker job 执行预算时,worker 已停止续租,provider 才超时或开始重试;最终 lease 过期、任务失败并退款,上游却可能继续消耗资源或迟到成功。
@@ -1,6 +1,6 @@
# 旧创作模板业务退役方案
更新时间:`2026-07-18`
更新时间:`2026-07-21`
## 目标
@@ -55,6 +55,7 @@
-`public_work_asset_read_grant` view 与十类旧作品授权计算退出 module;匿名资产读取只保留现役 editor showcase 授权。
- `spacetime-module``spacetime-client` 的 Cargo `lib.path` 固定指向各自的 `src/active.rs`;原 `src/lib.rs` 及旧业务源码继续原位保留,但不再作为 crate 根参与编译。
- 历史表最小定义集中在 `spacetime-module/src/legacy_schema/``spacetime-module/src/runtime/legacy_schema/`,混合 profile 表的在运数据壳位于 `spacetime-module/src/runtime/active/profile.rs`;这些目录只允许 schema 和必要兼容读取定义。
- SpacetimeDB schema guard 比较当前工作树与基线提交时,两侧都必须分别读取各自 `Cargo.toml``lib.path`,再沿 `mod` / `#[path]` 只扫描该快照 crate root 可达的 schema;不得递归扫描整个 `src/`,否则原位保留的旧源码会与现役历史数据壳产生假 accessor 重复。
- `module-runtime` 仍是账号、钱包、公共设置、追踪和 feature gate 的现役领域 crate;其混合源码中的 `CreationEntry*`、旧公开作品、旧存档 / 浏览历史 / 游玩统计 DTO、command、mapper 和规则必须以编译条件退出,且不再依赖只为旧创作契约存在的 `shared-contracts`。历史 schema 只继续编译 `RuntimeBrowseHistoryThemeMode` 六个变体和完整保序的 `RuntimeProfileWalletLedgerSourceType` 等持久化 ABI,不保留围绕这些类型的旧业务实现。
- 纯模板 crate 和专属运行态 crate 不属于 workspace members、default members 或任何在运 crate 的依赖图;源码目录保持原样。
- `platform-agent` 及其专属 `langchainrust` 依赖同样退出 workspace 与 `api-server` 依赖图;现役编辑器 Agent 仅需的模型常量收口到 `platform-llm`,不再通过旧拼图 Phase 1 / Creative Agent 执行器 crate 复用。
+48 -24
View File
@@ -55,8 +55,7 @@ function resolveBaseRef() {
return 'HEAD';
}
function resolveCurrentCrateRoot() {
const manifest = readFileSync(join(repoRoot, moduleManifestPath), 'utf8');
export function resolveCrateRootFromManifest(manifest) {
const libSection = /\[lib\]\s*\n([\s\S]*?)(?=\n\[|$)/u.exec(manifest)?.[1] ?? '';
const configuredPath = /^\s*path\s*=\s*"([^"]+)"/mu.exec(libSection)?.[1];
return normalizePath(
@@ -79,8 +78,9 @@ function childModuleDirectory(sourcePath, isCrateRoot) {
return join(dirname(sourcePath), fileName.slice(0, -'.rs'.length));
}
function listReachableCurrentRustFiles() {
const crateRoot = resolveCurrentCrateRoot();
export function listReachableRustFiles(readSource) {
const manifest = readSource(moduleManifestPath) ?? '';
const crateRoot = resolveCrateRootFromManifest(manifest);
const pending = [{ path: crateRoot, isCrateRoot: true }];
const visited = new Set();
const externalModulePattern = /((?:[ \t]*#\[[^\]\r\n]*\][ \t]*\r?\n)*)[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?mod[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*;/gmu;
@@ -91,13 +91,12 @@ function listReachableCurrentRustFiles() {
continue;
}
const absolutePath = join(repoRoot, current.path);
if (!existsSync(absolutePath)) {
const source = readSource(current.path);
if (source === null) {
continue;
}
visited.add(current.path);
const source = readFileSync(absolutePath, 'utf8');
const defaultModuleDir = childModuleDirectory(current.path, current.isCrateRoot);
let match;
@@ -118,7 +117,7 @@ function listReachableCurrentRustFiles() {
];
const modulePath = candidates
.map(normalizePath)
.find((candidate) => existsSync(join(repoRoot, candidate)));
.find((candidate) => readSource(candidate) !== null);
if (modulePath && !visited.has(modulePath)) {
pending.push({ path: modulePath, isCrateRoot: false });
@@ -129,26 +128,48 @@ function listReachableCurrentRustFiles() {
return [...visited].sort();
}
function listBaseRustFiles(baseRef) {
const output = tryGit(['ls-tree', '-r', '--name-only', baseRef, '--', moduleSrcRoot]);
function listBaseSourcePaths(baseRef) {
const output = tryGit([
'ls-tree',
'-r',
'--name-only',
baseRef,
'--',
moduleManifestPath,
moduleSrcRoot,
]);
if (!output) {
return [];
return new Set();
}
return output
.split(/\r?\n/u)
.map(normalizePath)
.filter((path) => path.endsWith('.rs'))
.sort();
return new Set(output.split(/\r?\n/u).map(normalizePath).filter(Boolean));
}
function readCurrentFile(path) {
if (!existsSync(join(repoRoot, path))) {
return null;
}
return readFileSync(join(repoRoot, path), 'utf8');
}
function readBaseFile(baseRef, path) {
const text = tryGit(['show', `${baseRef}:${path}`]);
return text ?? '';
return tryGit(['show', `${baseRef}:${path}`]);
}
function createBaseSourceReader(baseRef) {
const sourcePaths = listBaseSourcePaths(baseRef);
const sourceCache = new Map();
return (path) => {
const normalizedPath = normalizePath(path);
if (!sourcePaths.has(normalizedPath)) {
return null;
}
if (!sourceCache.has(normalizedPath)) {
sourceCache.set(normalizedPath, readBaseFile(baseRef, normalizedPath));
}
return sourceCache.get(normalizedPath);
};
}
function lineNumberAt(text, index) {
@@ -486,7 +507,7 @@ function parseTablesFromFile(path, text) {
return tables;
}
function collectTablesFromSources(sources) {
export function collectTablesFromSources(sources) {
const tables = new Map();
const failures = [];
@@ -507,16 +528,17 @@ function collectTablesFromSources(sources) {
}
function loadCurrentSources() {
return listReachableCurrentRustFiles().map((path) => ({
return listReachableRustFiles(readCurrentFile).map((path) => ({
path,
text: readCurrentFile(path),
text: readCurrentFile(path) ?? '',
}));
}
function loadBaseSources(baseRef) {
return listBaseRustFiles(baseRef).map((path) => ({
const readSource = createBaseSourceReader(baseRef);
return listReachableRustFiles(readSource).map((path) => ({
path,
text: readBaseFile(baseRef, path),
text: readSource(path) ?? '',
}));
}
@@ -696,4 +718,6 @@ function main() {
);
}
main();
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import {
collectTablesFromSources,
listReachableRustFiles,
} from './check-spacetime-schema-guard.mjs';
const manifestPath = 'server-rs/crates/spacetime-module/Cargo.toml';
const sourceRoot = 'server-rs/crates/spacetime-module/src';
type SourceFiles = Record<string, string>;
function createSourceReader(files: SourceFiles) {
const sources = new Map(Object.entries(files));
return (path: string) => sources.get(path) ?? null;
}
function collectReachableTables(files: SourceFiles) {
const readSource = createSourceReader(files);
const paths = listReachableRustFiles(readSource);
return {
paths,
result: collectTablesFromSources(
paths.map((path: string) => ({ path, text: readSource(path) ?? '' })),
),
};
}
function tableSource(structName: string, accessor: string) {
return `#[spacetimedb::table(accessor = ${accessor})]\npub struct ${structName} {\n pub id: u64,\n}\n`;
}
describe('SpacetimeDB schema guard module reachability', () => {
it('ignores duplicate accessors in retained but unreachable legacy sources', () => {
const activePath = `${sourceRoot}/active.rs`;
const shellPath = `${sourceRoot}/legacy_schema/example.rs`;
const retiredPath = `${sourceRoot}/example.rs`;
const { paths, result } = collectReachableTables({
[manifestPath]: '[lib]\npath = "src/active.rs"\n',
[activePath]: '#[path = "legacy_schema/example.rs"]\nmod example;\n',
[shellPath]: tableSource('ExampleSchemaShell', 'example'),
[retiredPath]: tableSource('RetiredExample', 'example'),
});
expect(paths).toEqual([activePath, shellPath]);
expect(result.failures).toEqual([]);
expect([...result.tables.keys()]).toEqual(['example']);
});
it('still rejects duplicate accessors when both definitions are reachable', () => {
const activePath = `${sourceRoot}/active.rs`;
const firstPath = `${sourceRoot}/first.rs`;
const secondPath = `${sourceRoot}/second.rs`;
const { result } = collectReachableTables({
[manifestPath]: '[lib]\npath = "src/active.rs"\n',
[activePath]: 'mod first;\nmod second;\n',
[firstPath]: tableSource('FirstExample', 'example'),
[secondPath]: tableSource('SecondExample', 'example'),
});
expect(result.failures).toHaveLength(1);
expect(result.failures[0]).toMatch(/table accessor example /u);
});
});