合并最新 master 分支
同步 SpacetimeDB schema guard 基线扫描修复。 同步相关测试、排障记忆与融合方案文档。
This commit is contained in:
@@ -3241,6 +3241,14 @@
|
||||
- 处理:把仍在用的公共账号 / 钱包 / 设置能力迁到明确的现役 client 与 presentation model;Vite `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 复用。
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user