收紧AGC资源kind枚举边界

canvas生成与运行时统一使用共享GameCreationAppAssetKind

删除平台kind兼容映射与退役kind拒绝测试

更新资源kind审计里程碑与绑定文档
This commit is contained in:
2026-09-19 01:28:27 +08:00
parent 0d5940433d
commit 48caa23997
68 changed files with 729 additions and 804 deletions
@@ -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'),
)
);
}