7afec12958
拆出 CLI、命令、配置、Agent、素材、项目、预览和窗口模块 拆出 Rust 单测并保留 main.rs 作为 Tauri 薄入口 扩展壳配置与原生壳门禁的 Rust 源码扫描范围 同步 AI 游戏创作 App 技术方案和决策记录
679 lines
20 KiB
JavaScript
679 lines
20 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
const packageConfig = JSON.parse(
|
|
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
);
|
|
const tauriConfig = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('../src-tauri/tauri.conf.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const defaultAppConfig = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('../game-creator.config.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const rootPackageConfig = JSON.parse(
|
|
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
|
|
);
|
|
const viteConfigSource = fs.readFileSync(
|
|
new URL('../vite.config.ts', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const appInvokeSource = fs.readFileSync(
|
|
new URL('../src/App.tsx', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const tauriHandlerSource = fs.readFileSync(
|
|
new URL('../src-tauri/src/main.rs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const tauriRustSource = readSourceTree(
|
|
new URL('../src-tauri/src/', import.meta.url),
|
|
'.rs',
|
|
);
|
|
const sharedContractSource = fs.readFileSync(
|
|
new URL(
|
|
'../../../packages/shared/src/contracts/gameCreationApp.ts',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const rustSharedContractSource = fs.readFileSync(
|
|
new URL(
|
|
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const allowedUncalledTauriCommands = [];
|
|
const sourceExtensions = new Set([
|
|
'.json',
|
|
'.md',
|
|
'.mjs',
|
|
'.rs',
|
|
'.toml',
|
|
'.ts',
|
|
'.tsx',
|
|
]);
|
|
|
|
function collectFiles(path) {
|
|
const stat = fs.statSync(path);
|
|
if (stat.isDirectory()) {
|
|
return fs.readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
|
|
if (entry.name === 'node_modules' || entry.name === 'target') {
|
|
return [];
|
|
}
|
|
return collectFiles(
|
|
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
|
|
);
|
|
});
|
|
}
|
|
if (sourceExtensions.has(pathnameExtension(path.pathname))) {
|
|
return [path];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function readSourceTree(path, extension) {
|
|
const stat = fs.statSync(path);
|
|
if (stat.isDirectory()) {
|
|
return fs
|
|
.readdirSync(path, { withFileTypes: true })
|
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
.map((entry) =>
|
|
readSourceTree(
|
|
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
|
|
extension,
|
|
),
|
|
)
|
|
.join('\n');
|
|
}
|
|
if (pathnameExtension(path.pathname) !== extension) {
|
|
return '';
|
|
}
|
|
return fs.readFileSync(path, 'utf8');
|
|
}
|
|
|
|
function pathnameExtension(pathname) {
|
|
const index = pathname.lastIndexOf('.');
|
|
return index === -1 ? '' : pathname.slice(index);
|
|
}
|
|
|
|
function assertNoOpenAiApiKeys(paths) {
|
|
const secretPattern = /sk-[A-Za-z0-9_-]{20,}/;
|
|
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
|
|
const source = fs.readFileSync(path, 'utf8');
|
|
if (secretPattern.test(source)) {
|
|
throw new Error(
|
|
`AI game creator shell source must not contain API keys: ${path.pathname}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoEnvironmentConfigFallbacks(paths) {
|
|
const allowedDevCheck = 'import.meta.env.DEV';
|
|
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
|
|
const source = fs
|
|
.readFileSync(path, 'utf8')
|
|
.replaceAll(allowedDevCheck, '')
|
|
.replaceAll('game-creator.config.local.json', '');
|
|
if (/\bprocess\.env\b|\bdotenv\b/.test(source)) {
|
|
throw new Error(
|
|
`AI game creator shell must use runtime config, not environment config: ${path.pathname}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoNativeBrowserConfirm(paths) {
|
|
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
|
|
const source = fs.readFileSync(path, 'utf8');
|
|
if (/\bwindow\.confirm\b/.test(source)) {
|
|
throw new Error(
|
|
`AI game creator shell confirmations must use in-app UI: ${path.pathname}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function extractConstArrayBlock(source, name) {
|
|
const start = source.indexOf(`const ${name}`);
|
|
if (start === -1) {
|
|
throw new Error(`Missing contract array: ${name}`);
|
|
}
|
|
const end = source.indexOf('];', start);
|
|
if (end === -1) {
|
|
throw new Error(`Missing contract array end: ${name}`);
|
|
}
|
|
return source.slice(start, end + 2);
|
|
}
|
|
|
|
function parseTsCommands(source) {
|
|
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
|
|
return Array.from(
|
|
block.matchAll(
|
|
/\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g,
|
|
),
|
|
([, id, permission]) => ({ id, permission }),
|
|
);
|
|
}
|
|
|
|
function parseRustCommands(source) {
|
|
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
|
|
const permissionNames = {
|
|
Auto: 'auto',
|
|
Confirm: 'confirm',
|
|
Deny: 'deny',
|
|
};
|
|
return Array.from(
|
|
block.matchAll(
|
|
/command\(\s*"([^"]+)",\s*GameCreationAppPermission::(Auto|Confirm|Deny)\s*\)/g,
|
|
),
|
|
([, id, permission]) => ({ id, permission: permissionNames[permission] }),
|
|
);
|
|
}
|
|
|
|
function parseTsCapabilities(source) {
|
|
const block = extractConstArrayBlock(
|
|
source,
|
|
'GAME_CREATION_AGENT_CAPABILITIES',
|
|
);
|
|
return Array.from(
|
|
block.matchAll(
|
|
/\{\s*id:\s*'([^']+)',\s*area:\s*'([^']+)',\s*title:\s*'([^']+)',?\s*\}/g,
|
|
),
|
|
([, id, area, title]) => ({ id, area, title }),
|
|
);
|
|
}
|
|
|
|
function parseRustCapabilities(source) {
|
|
const block = extractConstArrayBlock(
|
|
source,
|
|
'GAME_CREATION_AGENT_CAPABILITIES',
|
|
);
|
|
return Array.from(
|
|
block.matchAll(
|
|
/capability\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)",?\s*\)/g,
|
|
),
|
|
([, id, area, title]) => ({ id, area, title }),
|
|
);
|
|
}
|
|
|
|
function assertContractRecordsMatch(label, leftRecords, rightRecords) {
|
|
const normalize = (records) =>
|
|
records
|
|
.map((record) => JSON.stringify(record))
|
|
.sort((left, right) => left.localeCompare(right));
|
|
const left = normalize(leftRecords);
|
|
const right = normalize(rightRecords);
|
|
if (left.length === 0 || right.length === 0) {
|
|
throw new Error(`${label} parser returned no records`);
|
|
}
|
|
if (JSON.stringify(left) !== JSON.stringify(right)) {
|
|
throw new Error(
|
|
`${label} drifted between TypeScript and Rust contracts\nTS=${left.join(
|
|
'\n',
|
|
)}\nRust=${right.join('\n')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function parseAppInvokeCommandNames(source) {
|
|
return Array.from(
|
|
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
|
|
([, command]) => command,
|
|
);
|
|
}
|
|
|
|
function parseTauriHandlerCommandNames(source) {
|
|
const match = source.match(/tauri::generate_handler!\[([\s\S]*?)\]/);
|
|
if (!match) {
|
|
throw new Error('AI game creator shell Tauri handler list is missing');
|
|
}
|
|
return Array.from(
|
|
match[1].matchAll(/\b([a-z][a-z0-9_]+)\b/g),
|
|
([, command]) => command,
|
|
);
|
|
}
|
|
|
|
function parseRustFunctionNames(source) {
|
|
return Array.from(
|
|
source.matchAll(/\b(?:async\s+)?fn\s+([a-z][a-z0-9_]*)\s*\(/g),
|
|
([, name]) => name,
|
|
);
|
|
}
|
|
|
|
function assertCommandNamesSubset(label, leftNames, rightNames) {
|
|
const right = new Set(rightNames);
|
|
const missing = Array.from(new Set(leftNames))
|
|
.filter((name) => !right.has(name))
|
|
.sort((left, rightName) => left.localeCompare(rightName));
|
|
if (missing.length > 0) {
|
|
throw new Error(`${label} missing commands: ${missing.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
assertNoOpenAiApiKeys([
|
|
new URL('../src/', import.meta.url),
|
|
new URL('../scripts/', import.meta.url),
|
|
new URL('../game-creator.config.json', import.meta.url),
|
|
new URL('../src-tauri/src/', import.meta.url),
|
|
new URL('../package.json', import.meta.url),
|
|
new URL('../vite.config.ts', import.meta.url),
|
|
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
|
new URL('../src-tauri/tauri.conf.json', import.meta.url),
|
|
new URL(
|
|
'../../../packages/shared/src/contracts/gameCreationApp.ts',
|
|
import.meta.url,
|
|
),
|
|
new URL(
|
|
'../../../packages/shared/src/contracts/gameCreationApp.test.ts',
|
|
import.meta.url,
|
|
),
|
|
new URL(
|
|
'../../../server-rs/crates/platform-agent/src/game_creation.rs',
|
|
import.meta.url,
|
|
),
|
|
new URL(
|
|
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
|
|
import.meta.url,
|
|
),
|
|
new URL(
|
|
'../../../docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md',
|
|
import.meta.url,
|
|
),
|
|
]);
|
|
|
|
assertNoEnvironmentConfigFallbacks([
|
|
new URL('../src/', import.meta.url),
|
|
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
|
|
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
|
|
new URL('../scripts/start-dev-server.mjs', import.meta.url),
|
|
new URL('../src-tauri/src/', import.meta.url),
|
|
new URL('../package.json', import.meta.url),
|
|
new URL('../vite.config.ts', import.meta.url),
|
|
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
|
new URL('../src-tauri/tauri.conf.json', import.meta.url),
|
|
]);
|
|
|
|
assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]);
|
|
|
|
assertContractRecordsMatch(
|
|
'AI game creator shell command contract',
|
|
parseTsCommands(sharedContractSource),
|
|
parseRustCommands(rustSharedContractSource),
|
|
);
|
|
|
|
assertContractRecordsMatch(
|
|
'AI game creator shell capability contract',
|
|
parseTsCapabilities(sharedContractSource),
|
|
parseRustCapabilities(rustSharedContractSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell Tauri handler',
|
|
parseAppInvokeCommandNames(appInvokeSource),
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell Tauri command implementation',
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
parseRustFunctionNames(tauriRustSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell App invoke or explicit native-only allowlist',
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
[
|
|
...parseAppInvokeCommandNames(appInvokeSource),
|
|
...allowedUncalledTauriCommands,
|
|
],
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell explicit native-only allowlist',
|
|
allowedUncalledTauriCommands,
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
);
|
|
|
|
if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
|
|
throw new Error('AI game creator shell package name drifted');
|
|
}
|
|
|
|
if (
|
|
packageConfig.scripts?.['llm-status'] !==
|
|
'node scripts/run-cli-with-config.mjs --llm-status'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell llm-status must use client config before checking LLM config',
|
|
);
|
|
}
|
|
|
|
if (
|
|
packageConfig.scripts?.['agent-run'] !==
|
|
'node scripts/run-cli-with-config.mjs --agent-run'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell agent-run must use client config before running the provider path',
|
|
);
|
|
}
|
|
|
|
if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
|
|
throw new Error('AI game creator shell productName drifted');
|
|
}
|
|
|
|
if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
|
throw new Error('AI game creator shell identifier drifted');
|
|
}
|
|
|
|
if (tauriConfig.app?.withGlobalTauri !== true) {
|
|
throw new Error(
|
|
'AI game creator shell must expose window.__TAURI__ for local commands',
|
|
);
|
|
}
|
|
|
|
if (
|
|
!Array.isArray(tauriConfig.app?.windows) ||
|
|
tauriConfig.app.windows.length !== 1 ||
|
|
tauriConfig.app.windows[0]?.label !== 'launcher' ||
|
|
tauriConfig.app.windows[0]?.url !== 'index.html?launcher'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell must start with only the launcher window',
|
|
);
|
|
}
|
|
|
|
if (defaultAppConfig.llm?.apiKey !== '') {
|
|
throw new Error('AI game creator shell default llm.apiKey must stay empty');
|
|
}
|
|
|
|
if (defaultAppConfig.editorApi?.apiKey !== '') {
|
|
throw new Error(
|
|
'AI game creator shell default editorApi.apiKey must stay empty',
|
|
);
|
|
}
|
|
|
|
if (
|
|
defaultAppConfig.llm?.requestTimeoutMs < 1000 ||
|
|
defaultAppConfig.llm?.maxRetries < 0 ||
|
|
defaultAppConfig.llm?.retryBackoffMs < 1
|
|
) {
|
|
throw new Error('AI game creator shell default LLM timing config is invalid');
|
|
}
|
|
|
|
const windows = tauriConfig.app?.windows ?? [];
|
|
if (
|
|
windows.length !== 1 ||
|
|
windows[0]?.label !== 'launcher' ||
|
|
windows[0]?.url !== 'index.html?launcher'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell release config must expose only the launcher window',
|
|
);
|
|
}
|
|
|
|
const launcherWindow = windows[0];
|
|
if (
|
|
launcherWindow.width !== 820 ||
|
|
launcherWindow.height !== 640 ||
|
|
launcherWindow.minWidth !== 720 ||
|
|
launcherWindow.minHeight !== 520
|
|
) {
|
|
throw new Error('AI game creator shell launcher window must stay compact');
|
|
}
|
|
|
|
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
|
|
throw new Error(
|
|
'AI game creator shell Tauri devUrl must stay on the fixed Vite dev port',
|
|
);
|
|
}
|
|
|
|
if (!viteConfigSource.includes("host: '127.0.0.1'")) {
|
|
throw new Error(
|
|
'AI game creator shell Vite dev server must bind to localhost',
|
|
);
|
|
}
|
|
|
|
if (!viteConfigSource.includes('port: 3080')) {
|
|
throw new Error(
|
|
'AI game creator shell Vite dev port must match Tauri devUrl',
|
|
);
|
|
}
|
|
|
|
if (!viteConfigSource.includes('strictPort: true')) {
|
|
throw new Error(
|
|
'AI game creator shell Vite dev server must not drift away from Tauri devUrl',
|
|
);
|
|
}
|
|
|
|
if (
|
|
!tauriConfig.build?.beforeDevCommand?.includes(
|
|
'run ai-game-creator-shell:dev-server',
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell beforeDevCommand must reuse or start the fixed Vite dev server',
|
|
);
|
|
}
|
|
|
|
if (
|
|
!tauriConfig.build?.beforeBuildCommand?.includes('--config vite.config.ts')
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell beforeBuildCommand must resolve vite config from app root',
|
|
);
|
|
}
|
|
|
|
const devServerSource = fs.readFileSync(
|
|
new URL('../scripts/start-dev-server.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const runCliWithConfigSource = fs.readFileSync(
|
|
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
|
|
for (const snippet of [
|
|
'const port = 3080',
|
|
"response.body.includes('<title>AI 游戏创作</title>')",
|
|
'function isPortListening()',
|
|
'reuse existing Vite dev server',
|
|
'non-HTTP or unrecognized server',
|
|
"'--config', 'vite.config.ts'",
|
|
]) {
|
|
if (!devServerSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell dev server wrapper drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const snippet of [
|
|
"new URL('..', import.meta.url)",
|
|
"'--manifest-path'",
|
|
"'src-tauri/Cargo.toml'",
|
|
]) {
|
|
if (!runCliWithConfigSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell config CLI wrapper drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const snippet of [
|
|
'const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json"',
|
|
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
|
|
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
|
|
'fn configure_game_creator_runtime_config_dir(',
|
|
'app.path().app_config_dir()?',
|
|
'fn load_game_creator_app_config()',
|
|
'fn read_game_creator_app_config()',
|
|
'fn write_game_creator_app_config(',
|
|
'fn writable_game_creator_config_path()',
|
|
'fn normalize_game_creator_app_config(',
|
|
'fn merge_game_creator_config_file(',
|
|
'.join("apps")',
|
|
'.join("ai-game-creator-shell")',
|
|
'configure_game_creator_runtime_config_dir(app.handle())?',
|
|
'read_game_creator_app_config,',
|
|
'write_game_creator_app_config,',
|
|
'resolve_game_creator_llm_config_for_agent(app_config, "planner")',
|
|
'resolve_game_creator_llm_config_for_agent(app_config, "generator")',
|
|
'build_game_creator_llm_client_from_llm_config(&planner_llm, "agentLlm.planner")?',
|
|
'build_game_creator_llm_client_from_llm_config(&generator_llm, "agentLlm.generator")?',
|
|
'agentLlm.{agent_id}',
|
|
'let app_config = match load_game_creator_app_config()',
|
|
'fn append_local_permission_log_at(',
|
|
'"command.auto"',
|
|
'GameCreationAppPermission::Auto',
|
|
]) {
|
|
if (!tauriRustSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell developer window guardrail drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const snippet of [
|
|
'open_developer_window(app)?;',
|
|
'tauri::WebviewWindowBuilder::new(app, "developer"',
|
|
]) {
|
|
if (tauriRustSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell must not auto-open developer windows: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const smokeAgentRunSource = fs.readFileSync(
|
|
new URL('./smoke-agent-run-local-provider.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
for (const snippet of [
|
|
'agentLlm',
|
|
'planner-smoke-model',
|
|
'generator-smoke-model',
|
|
'global-smoke-model-unused',
|
|
]) {
|
|
if (!smokeAgentRunSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator local-provider smoke lost per-agent LLM coverage: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const appSource = fs.readFileSync(
|
|
new URL('../src/App.tsx', import.meta.url),
|
|
'utf8',
|
|
);
|
|
for (const snippet of [
|
|
"'game.run_local'",
|
|
"'read_game_creator_app_config'",
|
|
"'write_game_creator_app_config'",
|
|
'aria-label="运行时配置"',
|
|
'LLM API Key',
|
|
'画板 API Key',
|
|
'runtime_config.save',
|
|
"'/run:运行自检,启动本地 HTTP 预览并交给外部浏览器'",
|
|
'async function openPreviewInExternalBrowser',
|
|
"'open_local_game_preview'",
|
|
'已交给外部浏览器打开。',
|
|
'async function executeRunLocal',
|
|
'function needsInitializedChatProject',
|
|
'function resolvePendingCommandProjectPath',
|
|
'resolveChatProjectPath(localProject) ?? draftProjectPath',
|
|
'`permission.cancel ${command.id} missing-project`',
|
|
"'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'",
|
|
"'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'",
|
|
'function parseRememberInput',
|
|
"'/trace 或 /loop:查看最近一次 Agent loop trace'",
|
|
'async function executeAgentTraceChat',
|
|
"relativePath: '.agent/logs/command.log'",
|
|
"'permission.pending'",
|
|
"'permission.confirm'",
|
|
"'permission.cancel'",
|
|
"'command.auto'",
|
|
"'agent.run_status'",
|
|
'function summarizeAgentRunTrace',
|
|
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
|
|
'agentRunTrace.error ?',
|
|
'className="trace-error"',
|
|
'agentRunTrace.taskGraph.repairRoutes.map',
|
|
"in: ${step.inputPaths.join(', ') || 'none'}",
|
|
"out: ${step.outputPaths.join(', ') || 'none'}",
|
|
]) {
|
|
if (!appSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell trace panel guardrail drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const script of [
|
|
'ai-game-creator-shell:dev',
|
|
'ai-game-creator-shell:dev-server',
|
|
'ai-game-creator-shell:build',
|
|
'ai-game-creator-shell:typecheck',
|
|
'ai-game-creator-shell:agent-run:smoke',
|
|
'ai-game-creator-shell:check',
|
|
]) {
|
|
if (!rootPackageConfig.scripts?.[script]) {
|
|
throw new Error(`root package missing ${script}`);
|
|
}
|
|
}
|
|
|
|
if (
|
|
rootPackageConfig.scripts?.['ai-game-creator-shell:build'] !==
|
|
'npm --prefix apps/ai-game-creator-shell run build --'
|
|
) {
|
|
throw new Error(
|
|
'root ai-game-creator-shell:build script must forward build args',
|
|
);
|
|
}
|
|
|
|
const agentRunSmokeSource = fs.readFileSync(
|
|
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
|
|
for (const snippet of [
|
|
"const smokeAssetMarker = 'SMOKE_LOCAL_ASSET:chef'",
|
|
"const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3'",
|
|
"mediaType: 'audio/mpeg'",
|
|
'document.body.dataset.smokeFrame = String(frameCount)',
|
|
"ctx.fillStyle = '#ff00ff'",
|
|
'ctx.fillRect(4, 4, 8, 24)',
|
|
'const sample = ctx.getImageData(4, 4, 24, 24).data',
|
|
'chef.complete && chef.naturalWidth > 0',
|
|
'document.body.dataset.smokeCanvasPixels = String(litPixels)',
|
|
'document.body.dataset.smokeCanvasColors = String(colors.size)',
|
|
'readBrowserDom(previewUrl)',
|
|
"extractDomNumber(previewDom, 'smoke-canvas-pixels')",
|
|
'--dump-dom',
|
|
'readHttpHead(new URL(smokeAssetPath, previewUrl).toString())',
|
|
'readHttpHead(new URL(smokeAudioAssetPath, previewUrl).toString())',
|
|
'function writeStreamingChatCompletion',
|
|
'requestJson?.stream === true',
|
|
`requestBodies.every((body) => body.includes('"stream":true'))`,
|
|
"const localConfigPath = path.join(appRoot, 'game-creator.config.local.json')",
|
|
"apiKind: 'openai_chat'",
|
|
'stream: true',
|
|
'await restoreOptionalFile(localConfigPath, previousLocalConfig)',
|
|
"method: 'HEAD'",
|
|
'previewAssetHead.contentLength === String(smokeAssetBytes.length)',
|
|
"previewAudioHead.contentType === 'audio/mpeg'",
|
|
"previewAssetHead.body === ''",
|
|
'trace missing professional group ${group}',
|
|
]) {
|
|
if (!agentRunSmokeSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell agent-run smoke drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|