收紧AGC资源kind枚举边界
canvas生成与运行时统一使用共享GameCreationAppAssetKind 删除平台kind兼容映射与退役kind拒绝测试 更新资源kind审计里程碑与绑定文档
This commit is contained in:
@@ -16,11 +16,13 @@ import {
|
||||
GAME_CREATION_APP_COMMANDS,
|
||||
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||||
GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION,
|
||||
GAME_CREATION_APP_UI_DESIGN_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
|
||||
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
type GameCreationAgentRunTrace,
|
||||
gameCreationAppAssetCategory,
|
||||
gameCreationAppAssetCategoryForKind,
|
||||
gameCreationAppAssetCategoryForRawKind,
|
||||
type GameCreationAppAssetManifestEntry,
|
||||
gameCreationAppAssetPersistedCategory,
|
||||
gameCreationAppAssetTags,
|
||||
@@ -751,32 +753,27 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('parses kind strictly and never aliases non-canonical values', () => {
|
||||
it('parses kind strictly and closes unknown values over unknown', () => {
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('character-animation');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('font');
|
||||
expect(GAME_CREATION_APP_ASSET_KINDS).toContain('unknown');
|
||||
|
||||
// 口径是严格接受:不 trim、不做大小写归一、不查别名表、不做迁移。
|
||||
for (const raw of [
|
||||
'game-background',
|
||||
'character-art',
|
||||
'ui-prototype',
|
||||
'art-spritesheet',
|
||||
'art-spritesheet-slice',
|
||||
'illustration',
|
||||
'UI',
|
||||
'UI-Prototype',
|
||||
'FONT',
|
||||
' image ',
|
||||
]) {
|
||||
expect(parseGameCreationAppAssetKind(raw, 'test.legacy')).toBe('unknown');
|
||||
expect(gameCreationAppAssetCategoryForKind(raw)).toBe('unclassified');
|
||||
// 口径是严格接受:只认精确的 canonical 值。
|
||||
for (const raw of ['future-kind', 'UI', 'FONT', ' image ']) {
|
||||
expect(parseGameCreationAppAssetKind(raw, 'test.kind-boundary')).toBe(
|
||||
'unknown',
|
||||
);
|
||||
expect(
|
||||
gameCreationAppAssetCategoryForRawKind(raw, 'test.kind-category'),
|
||||
).toBe('unclassified');
|
||||
}
|
||||
expect(parseGameCreationAppAssetKind('font', 'test.legacy')).toBe('font');
|
||||
expect(parseGameCreationAppAssetKind('character', 'test.legacy')).toBe(
|
||||
'character',
|
||||
expect(parseGameCreationAppAssetKind('font', 'test.kind-boundary')).toBe(
|
||||
'font',
|
||||
);
|
||||
expect(parseGameCreationAppAssetKind('unknown', 'test.legacy')).toBe(
|
||||
expect(
|
||||
parseGameCreationAppAssetKind('character', 'test.kind-boundary'),
|
||||
).toBe('character');
|
||||
expect(parseGameCreationAppAssetKind('unknown', 'test.kind-boundary')).toBe(
|
||||
'unknown',
|
||||
);
|
||||
|
||||
@@ -794,9 +791,9 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
expect(parseGameCreationAppAssetKind(prototypeKey, 'test.legacy')).toBe(
|
||||
'unknown',
|
||||
);
|
||||
expect(gameCreationAppAssetCategoryForKind(prototypeKey)).toBe(
|
||||
'unclassified',
|
||||
);
|
||||
expect(
|
||||
gameCreationAppAssetCategoryForRawKind(prototypeKey, 'test.legacy'),
|
||||
).toBe('unclassified');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -840,8 +837,13 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
// legacy / 非 canonical kind 一律严格收口,不被当作文档(判据只做等值匹配)。
|
||||
for (const raw of ['UI', 'ui', 'ui-design-doc ', 'UI-DESIGN-DOC']) {
|
||||
// 非 canonical kind 一律严格收口,不被当作文档(判据只做等值匹配)。
|
||||
for (const raw of [
|
||||
'UI',
|
||||
'future-kind',
|
||||
'ui-design-doc ',
|
||||
'UI-DESIGN-DOC',
|
||||
]) {
|
||||
expect(
|
||||
isGameCreationAppUiDesignDocAsset({
|
||||
kind: raw,
|
||||
@@ -872,14 +874,19 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
expect(gameCreationAppAssetCategoryForKind('scene')).toBe('scene');
|
||||
expect(gameCreationAppAssetCategoryForKind('sound-effect')).toBe('audio');
|
||||
expect(gameCreationAppAssetCategoryForKind('spec')).toBe('document');
|
||||
for (const kind of ['image', 'video', 'code', 'publication-material']) {
|
||||
for (const kind of [
|
||||
'image',
|
||||
'video',
|
||||
'code',
|
||||
'publication-material',
|
||||
] as const) {
|
||||
expect(gameCreationAppAssetCategoryForKind(kind)).toBe('unclassified');
|
||||
}
|
||||
// `font` 是 canonical 成员(现役写入侧直接写 `font`),分类是 `document`。
|
||||
expect(gameCreationAppAssetCategoryForKind('font')).toBe('document');
|
||||
expect(gameCreationAppAssetCategoryForKind('unknown-kind')).toBe(
|
||||
'unclassified',
|
||||
);
|
||||
expect(
|
||||
gameCreationAppAssetCategoryForRawKind('unknown-kind', 'test.unknown'),
|
||||
).toBe('unclassified');
|
||||
expect(gameCreationAppAssetCategoryForKind('unknown')).toBe('unclassified');
|
||||
});
|
||||
|
||||
@@ -896,7 +903,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
|
||||
// 有意行为(2026-09-10 分类自愈批次):落盘 `unclassified` 而 kind 能派生出明确的
|
||||
// 非 `unclassified` 分类时,读取侧采用派生值。这条规则用于自愈历史上被系统误写成
|
||||
// `unclassified` 的存量(真机 57 条 `kind:"ui"` 资产),无需迁移脚本且永久生效。
|
||||
// `unclassified` 的存量由 canonical kind 的读显示口径决定,无需迁移脚本且永久生效。
|
||||
// 已知盲区:用户手动把可明确分类的资产设为「待归类」时该手动值会被覆盖,属有意接受。
|
||||
expect(
|
||||
gameCreationAppAssetCategory({ ...legacy, category: 'unclassified' }),
|
||||
@@ -950,8 +957,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
* `unclassified` 的存量(真机出现过 UI 资产落盘 `unclassified` 而 kind 已是
|
||||
* `ui-design` 的情况),不写迁移脚本且永久生效。
|
||||
*
|
||||
* 这条规则**不含**任何 kind 别名归一:认不出的 kind 收口成 `unknown`,派生结果同样是
|
||||
* `unclassified`,规则不触发。
|
||||
* 认不出的 kind 收口成 `unknown`,派生结果同样是 `unclassified`,规则不触发。
|
||||
*/
|
||||
it('自愈规则覆盖落盘 unclassified:这是显式接受的盲区', () => {
|
||||
// 落盘 unclassified + kind 可明确分类 → 派生值覆盖落盘值。
|
||||
@@ -976,9 +982,12 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
category: 'unclassified',
|
||||
}),
|
||||
).toBe('ui-interaction');
|
||||
// legacy 原值不在自愈窗口内:它连 canonical kind 都不是。
|
||||
// unknown kind 不在自愈窗口内:它没有可派生的明确分类。
|
||||
expect(
|
||||
gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }),
|
||||
gameCreationAppAssetCategory({
|
||||
kind: 'future-kind',
|
||||
category: 'unclassified',
|
||||
}),
|
||||
).toBe('unclassified');
|
||||
});
|
||||
|
||||
@@ -1007,7 +1016,10 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
gameCreationAppAssetCategory({ kind: 'character', category: 'audio' }),
|
||||
).toBe('audio');
|
||||
expect(
|
||||
gameCreationAppAssetCategory({ kind: 'ui', category: 'document' }),
|
||||
gameCreationAppAssetCategory({
|
||||
kind: GAME_CREATION_APP_UI_DESIGN_ASSET_KIND,
|
||||
category: 'document',
|
||||
}),
|
||||
).toBe('document');
|
||||
});
|
||||
|
||||
@@ -1041,7 +1053,10 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
|
||||
// 落盘值合法且非 unclassified 时两个口径一致。
|
||||
expect(
|
||||
gameCreationAppAssetPersistedCategory({ kind: 'ui', category: 'audio' }),
|
||||
gameCreationAppAssetPersistedCategory({
|
||||
kind: GAME_CREATION_APP_UI_DESIGN_ASSET_KIND,
|
||||
category: 'audio',
|
||||
}),
|
||||
).toBe('audio');
|
||||
});
|
||||
|
||||
|
||||
@@ -518,13 +518,7 @@ export function isGameCreationAppUiDesignDocAsset(
|
||||
|
||||
/**
|
||||
* 运行期 kind 列表:穷举 Record 保证它不可能与生成 union 分叉(漏一个/多一个都编译报错)。
|
||||
* 键顺序即生成声明顺序。kind 字符串表**只有这一份运行期副本**,别名表已删除。
|
||||
*
|
||||
* kind 的**唯一真源**是 Rust 侧 `GameCreationAppAssetKind`(ts-rs 生成
|
||||
* `./generated/GameCreationAppAssetKind`)。旧的手写 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS`、
|
||||
* legacy 别名表与 `canonicalGameCreationAppAssetKind()` 已从本文件删除,Rust 侧同名的
|
||||
* `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` / `canonical_game_creation_app_asset_kind()`
|
||||
* 一并删除。口径是严格接受:不 trim、不大小写归一、不查别名、不做迁移。
|
||||
* 键顺序即生成声明顺序。kind 字符串表只有这一份运行期副本;输入解析只接受 canonical 值。
|
||||
*/
|
||||
const GAME_CREATION_APP_ASSET_KIND_MEMBERS: Record<
|
||||
GameCreationAppAssetKind,
|
||||
@@ -618,7 +612,7 @@ export function isGameCreationAppAssetKind(
|
||||
* 严格解析外部 kind 字符串(manifest / API):只认 canonical 值,认不出的收口成 `unknown`
|
||||
* 并把**原始串**打到 `console.warn`(Rust 侧同类留痕走 `app_log!`)。
|
||||
*
|
||||
* 不 trim、不做大小写归一、不查别名表、不做迁移:`"UI"` / `"ui"` / `" image "` 一律 `unknown`。
|
||||
* 只接受精确的 canonical 值,其他输入一律收口为 `unknown`。
|
||||
*/
|
||||
export function parseGameCreationAppAssetKind(
|
||||
raw: unknown,
|
||||
@@ -699,11 +693,19 @@ export function normalizeGameCreationAppAssetCategory(
|
||||
}
|
||||
|
||||
export function gameCreationAppAssetCategoryForKind(
|
||||
kind: string,
|
||||
kind: GameCreationAppAssetKind,
|
||||
): GameCreationAppAssetCategory {
|
||||
return GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND[
|
||||
parseGameCreationAppAssetKind(kind, 'asset-category.by-kind')
|
||||
];
|
||||
return GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND[kind];
|
||||
}
|
||||
|
||||
/** 解析外部 kind 后再按共享枚举派生分类;内部业务逻辑应传入已解析的枚举。 */
|
||||
export function gameCreationAppAssetCategoryForRawKind(
|
||||
raw: unknown,
|
||||
context: string,
|
||||
): GameCreationAppAssetCategory {
|
||||
return gameCreationAppAssetCategoryForKind(
|
||||
parseGameCreationAppAssetKind(raw, context),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -718,8 +720,7 @@ export function gameCreationAppAssetCategoryForKind(
|
||||
* **例外(唯一的覆盖窗口)**:落盘值是 `unclassified`,而该资产 `kind` 能派生出明确的
|
||||
* 非 `unclassified` 分类时,采用派生值。这条规则用于自愈「历史上被系统错误写成
|
||||
* unclassified」的存量数据——典型例子是 UI 资产落盘 `unclassified` 而 kind 已是 `ui-design`。
|
||||
* kind 认不出时只收口成 `unknown`(派生同样是 `unclassified`)并留痕原始串,
|
||||
* **不查别名表**:这条自愈规则不负责把 legacy kind 映射回 canonical。
|
||||
* kind 认不出时只收口成 `unknown`(派生同样是 `unclassified`)并留痕原始串。
|
||||
*
|
||||
* 盲区:用户**手动**把一个 kind 已能明确分类的资产设成「待归类」时,该手动值会被这条
|
||||
* 规则覆盖。这是有意接受的取舍——「手动设为待归类」本身意图边缘,且 kind 已经表达了分类;
|
||||
@@ -738,7 +739,9 @@ export function gameCreationAppAssetCategory(
|
||||
asset: Pick<GameCreationAppAssetManifestEntry, 'kind' | 'category'>,
|
||||
): GameCreationAppAssetCategory {
|
||||
const persisted = normalizeGameCreationAppAssetCategory(asset.category);
|
||||
const derivedFromKind = gameCreationAppAssetCategoryForKind(asset.kind);
|
||||
const derivedFromKind = gameCreationAppAssetCategoryForKind(
|
||||
parseGameCreationAppAssetKind(asset.kind, 'asset-category.display'),
|
||||
);
|
||||
if (persisted === null) {
|
||||
return derivedFromKind;
|
||||
}
|
||||
@@ -765,7 +768,9 @@ export function gameCreationAppAssetPersistedCategory(
|
||||
): GameCreationAppAssetCategory {
|
||||
return (
|
||||
normalizeGameCreationAppAssetCategory(asset.category) ??
|
||||
gameCreationAppAssetCategoryForKind(asset.kind)
|
||||
gameCreationAppAssetCategoryForKind(
|
||||
parseGameCreationAppAssetKind(asset.kind, 'asset-category.persisted'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user