Merge pull request #306 from GeWuYou/feat/ai-first-config

Feat/ai first config
This commit is contained in:
gewuyou 2026-04-30 16:09:43 +08:00 committed by GitHub
commit 36db7d0929
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1777 additions and 125 deletions

View File

@ -27,6 +27,29 @@
- `IUiFactory``ISceneFactory``IUiRoot``ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。 - `IUiFactory``ISceneFactory``IUiRoot``ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
- 常见做法也是这样组织:页面 / 场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。 - 常见做法也是这样组织:页面 / 场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。
## Config Workflow Boundary
If you only depend on `GFramework.Game.Abstractions`, keep the configuration boundary explicit.
`Config/` in this package defines read-side contracts such as `IConfigLoader`, `IConfigRegistry`, `IConfigTable`, and
diagnostic models. It does not define the full adoption boundary of the AI-First configuration workflow by itself.
The actual implementation and support boundary still lives in `GFramework.Game` and its companion documentation:
- `YamlConfigLoader`, `GameConfigBootstrap`, and `GameConfigModule` are runtime features from `GFramework.Game`
- `GFramework.Game.SourceGenerators` targets the shared schema subset that stays aligned with the runtime contract
- schema designs outside that shared subset should be evaluated against `GFramework.Game` and
`docs/zh-CN/game/config-system.md`, not inferred from abstractions alone
Typical examples that are outside the current adoption path include:
- combinators such as `oneOf` and `anyOf`
- non-`false` forms of `additionalProperties`
- other schema designs that rely on open object shapes, union-like branching, or shape-merging behavior
If your project needs those boundaries clarified, move from this package-level contract view to the runtime-facing
configuration documentation instead of assuming `Game.Abstractions` implies broader schema support.
## 子系统地图 ## 子系统地图
### `Config/` ### `Config/`
@ -205,6 +228,8 @@ public sealed class ContinueGameCommandHandler
- 槽位存档仓库实现 - 槽位存档仓库实现
- YAML 配置加载器 - YAML 配置加载器
- Scene / UI 路由基类 - Scene / UI 路由基类
- AI-First configuration boundary details, including the supported shared schema subset and the unsupported combinator /
open-shape cases
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。 也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
@ -251,3 +276,6 @@ public sealed class ContinueGameCommandHandler
- 你需要默认实现、基础设施拼装、运行时启动入口 - 你需要默认实现、基础设施拼装、运行时启动入口
- 两者一起用 - 两者一起用
- 最常见。公共层依赖 abstractions应用层或引擎层依赖 runtime - 最常见。公共层依赖 abstractions应用层或引擎层依赖 runtime
For configuration-specific adoption decisions, treat `GFramework.Game` and
[配置系统](../docs/zh-CN/game/config-system.md) as the authoritative next step.

View File

@ -19,3 +19,5 @@
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_015 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_016 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics

View File

@ -232,6 +232,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
} }
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) && return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) && TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) && TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) && TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
@ -844,6 +846,148 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
out diagnostic); out diagnostic);
} }
/// <summary>
/// 递归拒绝当前共享子集尚未支持的组合关键字。
/// 这里显式拦截 <c>oneOf</c> / <c>anyOf</c>,避免生成器静默接受会改变生成类型形状的 schema。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>当前节点树是否未声明不支持的组合关键字。</returns>
private static bool TryValidateUnsupportedCombinatorKeywordsRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, _) =>
{
return TryValidateUnsupportedCombinatorKeywords(
currentFilePath,
currentDisplayPath,
currentElement,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
/// <summary>
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>当前节点树是否未声明不支持的开放对象关键字形状。</returns>
private static bool TryValidateUnsupportedOpenObjectKeywordsRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, _) =>
{
return TryValidateUnsupportedOpenObjectKeywords(
currentFilePath,
currentDisplayPath,
currentElement,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
/// <summary>
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>未声明不支持关键字时返回 <see langword="true" />。</returns>
private static bool TryValidateUnsupportedCombinatorKeywords(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedCombinatorKeyword,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
keywordName,
"The current config schema subset does not support combinators that can change generated type shape.");
return false;
}
/// <summary>
/// 验证当前节点是否声明了当前共享子集尚未支持的开放对象关键字形状。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>未声明不支持关键字时返回 <see langword="true" />。</returns>
private static bool TryValidateUnsupportedOpenObjectKeywords(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
{
return true;
}
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedOpenObjectKeyword,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"additionalProperties",
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
return false;
}
/// <summary>
/// 返回当前节点声明的首个未支持组合关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
{
return element.TryGetProperty("oneOf", out _) ? "oneOf" :
element.TryGetProperty("anyOf", out _) ? "anyOf" :
null;
}
/// <summary> /// <summary>
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。 /// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> / /// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /

View File

@ -162,4 +162,26 @@ public static class ConfigSchemaDiagnostics
SourceGeneratorsConfigCategory, SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error, DiagnosticSeverity.Error,
true); true);
/// <summary>
/// schema 节点声明了当前共享子集尚未支持的组合关键字。
/// </summary>
public static readonly DiagnosticDescriptor UnsupportedCombinatorKeyword = new(
"GF_ConfigSchema_015",
"Config schema uses an unsupported combinator keyword",
"Property '{1}' in schema file '{0}' uses unsupported combinator keyword '{2}': {3}",
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
/// <summary>
/// schema 节点声明了当前共享子集尚未支持的开放对象关键字形状。
/// </summary>
public static readonly DiagnosticDescriptor UnsupportedOpenObjectKeyword = new(
"GF_ConfigSchema_016",
"Config schema uses an unsupported open-object keyword",
"Property '{1}' in schema file '{0}' uses unsupported open-object keyword '{2}': {3}",
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
} }

View File

@ -14,6 +14,9 @@
- 对应的表包装类型 - 对应的表包装类型
- 与 `GFramework.Game.Config` 运行时协作的访问辅助代码 - 与 `GFramework.Game.Config` 运行时协作的访问辅助代码
这里要先明确一条采用边界:`GFramework.Game.SourceGenerators` 服务的是当前与 `GFramework.Game`
Runtime 对齐的共享 schema 子集,而不是任意 `JSON Schema` 的全量实现。它的目标是让配置生成、运行时校验和工具链维持同一份可落地契约,而不是把所有 schema 组合能力都映射成生成类型。
## 包关系 ## 包关系
- 运行时:`GFramework.Game` - 运行时:`GFramework.Game`
@ -73,6 +76,15 @@ GameProject/
- 你希望在编译期拿到强类型配置访问入口 - 你希望在编译期拿到强类型配置访问入口
- 你希望运行时加载、schema 校验和编辑工具链共用同一份结构定义 - 你希望运行时加载、schema 校验和编辑工具链共用同一份结构定义
如果你的 schema 设计依赖下面这些场景,就不属于当前默认采用路径:
- `oneOf`
- `anyOf`
- 非 `false``additionalProperties`
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束
遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。
## 对应文档 ## 对应文档
- 配置系统:[配置系统文档](../docs/zh-CN/game/config-system.md) - 配置系统:[配置系统文档](../docs/zh-CN/game/config-system.md)

View File

@ -270,6 +270,181 @@ public sealed class YamlConfigLoaderAllOfTests
}); });
} }
/// <summary>
/// 验证运行时会显式拒绝当前共享子集尚未支持的 <c>oneOf</c>。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_OneOf()
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
"""
[
{
"type": "object",
"required": ["itemCount"],
"properties": {
"itemCount": { "type": "integer" }
}
}
]
""",
"""
"oneOf": [
{
"type": "object",
"required": ["bonus"],
"properties": {
"bonus": { "type": "integer" }
}
}
]
"""));
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
Assert.That(exception.Message, Does.Contain("unsupported combinator keyword 'oneOf'"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}
/// <summary>
/// 验证运行时会显式拒绝当前共享子集尚未支持的 <c>anyOf</c>。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_AnyOf()
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
"""
[
{
"type": "object",
"required": ["itemCount"],
"properties": {
"itemCount": { "type": "integer" }
}
}
]
""",
"""
"anyOf": [
{
"type": "object",
"required": ["bonus"],
"properties": {
"bonus": { "type": "integer" }
}
}
]
"""));
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
Assert.That(exception.Message, Does.Contain("unsupported combinator keyword 'anyOf'"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}
/// <summary>
/// 验证运行时接受显式声明的 <c>additionalProperties: false</c>
/// 因为这与当前闭合对象字段集语义保持一致。
/// </summary>
[Test]
public async Task LoadAsync_Should_Accept_When_Object_Schema_Declares_AdditionalProperties_False()
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
DefaultAllOfJson,
"""
"additionalProperties": false
"""));
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterAllOfConfigStub>("monster");
Assert.That(table.Count, Is.EqualTo(1));
}
/// <summary>
/// 验证运行时会拒绝会打开动态字段形状的 <c>additionalProperties</c>。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_AdditionalProperties()
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
DefaultAllOfJson,
"""
"additionalProperties": true
"""));
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
Assert.That(exception.Message, Does.Contain("unsupported 'additionalProperties' metadata"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}
/// <summary> /// <summary>
/// 验证 allOf 条目只接受 object-typed schema。 /// 验证 allOf 条目只接受 object-typed schema。
/// </summary> /// </summary>
@ -566,10 +741,12 @@ public sealed class YamlConfigLoaderAllOfTests
/// </summary> /// </summary>
/// <param name="rewardPropertiesJson">奖励对象的 properties JSON 片段。</param> /// <param name="rewardPropertiesJson">奖励对象的 properties JSON 片段。</param>
/// <param name="allOfJson">allOf 约束的 JSON 数组片段。</param> /// <param name="allOfJson">allOf 约束的 JSON 数组片段。</param>
/// <param name="additionalRewardKeywordsJson">追加到奖励对象上的额外关键字 JSON 片段。</param>
/// <returns>完整的 schema JSON 文本。</returns> /// <returns>完整的 schema JSON 文本。</returns>
private static string BuildMonsterSchema( private static string BuildMonsterSchema(
string rewardPropertiesJson, string rewardPropertiesJson,
string allOfJson) string allOfJson,
string additionalRewardKeywordsJson = "")
{ {
return $$""" return $$"""
{ {
@ -580,7 +757,7 @@ public sealed class YamlConfigLoaderAllOfTests
"reward": { "reward": {
"type": "object", "type": "object",
"properties": {{rewardPropertiesJson}}, "properties": {{rewardPropertiesJson}},
"allOf": {{allOfJson}} "allOf": {{allOfJson}}{{(string.IsNullOrWhiteSpace(additionalRewardKeywordsJson) ? string.Empty : "," + Environment.NewLine + additionalRewardKeywordsJson.Trim())}}
} }
} }
} }

View File

@ -321,6 +321,8 @@ internal static partial class YamlConfigSchemaValidator
JsonElement element, JsonElement element,
bool isRoot = false) bool isRoot = false)
{ {
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element);
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element); var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element); var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName); ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
@ -336,6 +338,81 @@ internal static partial class YamlConfigSchemaValidator
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element)); return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
} }
/// <summary>
/// 显式拒绝当前共享子集中尚未支持、且会改变生成类型形状的组合关键字。
/// 这样 Runtime / Generator / Tooling 会对同一份 schema 给出一致失败,
/// 而不是默默忽略 <c>oneOf</c> / <c>anyOf</c> 造成接受范围漂移。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
/// <param name="element">当前 schema 节点。</param>
private static void ValidateUnsupportedCombinatorKeywords(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported combinator keyword '{keywordName}'. " +
"The current config schema subset does not support combinators that can change generated type shape.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
/// <summary>
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
/// <param name="element">当前 schema 节点。</param>
private static void ValidateUnsupportedOpenObjectKeywords(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
{
return;
}
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported 'additionalProperties' metadata. " +
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
/// <summary>
/// 返回当前节点声明的首个未支持组合关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
{
return element.TryGetProperty("oneOf", out _) ? "oneOf" :
element.TryGetProperty("anyOf", out _) ? "anyOf" :
null;
}
/// <summary> /// <summary>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。 /// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </summary> /// </summary>

View File

@ -292,6 +292,13 @@ var registry = bootstrap.Registry;
- [内容配置系统](../docs/zh-CN/game/config-system.md) - [内容配置系统](../docs/zh-CN/game/config-system.md)
接入前建议先记住当前采用边界:
- 正式契约以 `YamlConfigLoader``GFramework.Game.SourceGenerators` 共享支持的 schema 子集为准
- `additionalProperties` 当前只接受 `false`,用于保持对象字段集闭合
- `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前不属于采用路径
- VS Code 配置工具是内容维护辅助层;如果 schema 超出共享子集,应回退到 raw YAML 与 schema 本体设计
### 4. 接入 Scene / UI 路由 ### 4. 接入 Scene / UI 路由
这里的最小前提不是“直接 new 一个 router”而是先补齐运行时依赖 这里的最小前提不是“直接 new 一个 router”而是先补齐运行时依赖

View File

@ -1795,6 +1795,173 @@ public class SchemaConfigGeneratorTests
}); });
} }
/// <summary>
/// 验证生成器会显式拒绝当前共享子集尚未支持的 <c>oneOf</c>。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_OneOf()
{
const string source = DummySource;
const string schema = """
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"properties": {
"itemCount": { "type": "integer" }
},
"oneOf": [
{
"type": "object",
"required": ["itemCount"],
"properties": {
"itemCount": { "type": "integer" }
}
}
]
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_015"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
Assert.That(diagnostic.GetMessage(), Does.Contain("oneOf"));
Assert.That(diagnostic.GetMessage(), Does.Contain("does not support combinators that can change generated type shape"));
});
}
/// <summary>
/// 验证生成器会显式拒绝当前共享子集尚未支持的 <c>anyOf</c>。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf()
{
const string source = DummySource;
const string schema = """
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"properties": {
"itemCount": { "type": "integer" }
},
"anyOf": [
{
"type": "object",
"required": ["itemCount"],
"properties": {
"itemCount": { "type": "integer" }
}
}
]
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_015"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
Assert.That(diagnostic.GetMessage(), Does.Contain("anyOf"));
Assert.That(diagnostic.GetMessage(), Does.Contain("does not support combinators that can change generated type shape"));
});
}
/// <summary>
/// 验证生成器接受显式声明的 <c>additionalProperties: false</c>。
/// </summary>
[Test]
public void Run_Should_Accept_When_Object_Schema_Declares_AdditionalProperties_False()
{
const string source = DummySource;
const string schema = """
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"additionalProperties": false,
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
}
/// <summary>
/// 验证生成器会拒绝会打开动态字段形状的 <c>additionalProperties</c>。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AdditionalProperties()
{
const string source = DummySource;
const string schema = """
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"additionalProperties": true,
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_016"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
Assert.That(diagnostic.GetMessage(), Does.Contain("additionalProperties"));
Assert.That(diagnostic.GetMessage(), Does.Contain("only accepts 'additionalProperties: false'"));
});
}
/// <summary> /// <summary>
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。 /// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
/// </summary> /// </summary>

View File

@ -15,6 +15,7 @@
- 想先跑一个最小例子:[快速开始](docs/zh-CN/getting-started/quick-start.md) - 想先跑一个最小例子:[快速开始](docs/zh-CN/getting-started/quick-start.md)
- 想先确认该装哪些包:[安装配置](docs/zh-CN/getting-started/installation.md) - 想先确认该装哪些包:[安装配置](docs/zh-CN/getting-started/installation.md)
- 想接入 AI-First 配置工作流:[配置系统](docs/zh-CN/game/config-system.md) / [VS Code 配置工具](docs/zh-CN/game/config-tool.md) - 想接入 AI-First 配置工作流:[配置系统](docs/zh-CN/game/config-system.md) / [VS Code 配置工具](docs/zh-CN/game/config-tool.md)
当前正式支持边界以 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 共享 schema 子集为准VS Code 工具是辅助层,不会单独扩出另一套配置契约
- 已经知道要用哪个模块:直接进入对应模块的说明页 - 已经知道要用哪个模块:直接进入对应模块的说明页
## 模块地图 ## 模块地图
@ -74,6 +75,13 @@
- `GeWuYou.GFramework.*.SourceGenerators` - `GeWuYou.GFramework.*.SourceGenerators`
只在需要编译期生成代码时安装,版本应与运行时包保持一致。 只在需要编译期生成代码时安装,版本应与运行时包保持一致。
如果你采用 AI-First 配置工作流,建议在进入实现前先确认两条边界:
- 当前共享子集接受闭合对象边界 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为不支持)
- `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前会被 Runtime / Generator / Tooling 直接拒绝
更复杂的 schema shape 需要先回到 schema 设计与 raw YAML 维护路径,而不是假定编辑器工具存在隐藏支持。
## 最小安装组合 ## 最小安装组合
```bash ```bash

View File

@ -6,6 +6,13 @@
当前阶段不再把 VS Code 工具能力当作阻塞项;工具链只要不拖累 C# 首发可用版本即可。 当前阶段不再把 VS Code 工具能力当作阻塞项;工具链只要不拖累 C# 首发可用版本即可。
## 并行 Lane 约束
- `C# Runtime + Source Generator + Consumer DX` 仍是当前主线恢复点
- Tooling / Docs 作为非阻塞并行 lane 单独推进,但每一批仍要和 Runtime / Generator 的共享关键字边界保持一致
- active tracking / trace 只保留恢复点、验证与 lane 指针;复杂编辑器细节、宿主手工验证和文档批次安排统一写在本文件
- public docs 只写消费者接入、限制和迁移边界;治理噪音、批次编排和 recovery 元数据继续留在 `ai-plan/**`
## 当前状态 ## 当前状态
- [x] 单表注册辅助:`Register{Entity}Table()` - [x] 单表注册辅助:`Register{Entity}Table()`
@ -37,8 +44,8 @@
- [x] 继续扩展最有价值的 JSON Schema 子集 - [x] 继续扩展最有价值的 JSON Schema 子集
- 原则:只做 Runtime / Generator / Tooling 三端都能稳定解释的关键字 - 原则:只做 Runtime / Generator / Tooling 三端都能稳定解释的关键字
- 已补齐:`enum`(当前覆盖标量、对象、数组节点,以及标量数组元素)、`const``not``pattern``format`(当前稳定子集:`date``date-time``duration``email``time``uri``uuid`)、`minItems`、`maxItems`、`exclusiveMinimum``exclusiveMaximum``multipleOf``uniqueItems``minProperties``maxProperties``dependentRequired``dependentSchemas``allOf`、object-focused `if` / `then` / `else` - 已补齐:`enum`(当前覆盖标量、对象、数组节点,以及标量数组元素)、`const``not``pattern``format`(当前稳定子集:`date``date-time``duration``email``time``uri``uuid`)、`minLength`、`maxLength``minItems`、`maxItems`、`contains``minContains``maxContains`、`exclusiveMinimum``exclusiveMaximum``multipleOf``uniqueItems``minProperties``maxProperties``dependentRequired``dependentSchemas``allOf`、object-focused `if` / `then` / `else`
- 当前产出运行时拒绝相关约束违规值VS Code 校验与表单 hint 对齐,生成代码 XML 文档同步暴露新关键字;对象 / 数组 `enum` 当前主要参与校验与文档输出,不额外扩展复杂表单控件;`allOf``if` / `then` / `else` 当前都收敛为 object-focused constraint block不做属性合并 - 当前产出运行时拒绝相关约束违规值VS Code 校验与表单 hint 对齐,生成代码 XML 文档同步暴露新关键字;对象 / 数组 `enum` 当前主要参与校验与文档输出,不额外扩展复杂表单控件;`allOf``if` / `then` / `else` 当前都收敛为 object-focused constraint block不做属性合并`oneOf` / `anyOf` 当前已统一定义为不支持并在三端显式拒绝
- [x] 评估可选只读索引能力 - [x] 评估可选只读索引能力
- 目标:为高频查询字段提供比 `All()` 线性扫描更强的读取体验 - 目标:为高频查询字段提供比 `All()` 线性扫描更强的读取体验
@ -60,10 +67,28 @@
- [ ] 继续扩插件的复杂表单能力 - [ ] 继续扩插件的复杂表单能力
- 说明:这是可选项,不阻塞 C# 主线 - 说明:这是可选项,不阻塞 C# 主线
## Tooling / Docs 并行 Lane
- [ ] Tooling让 VS Code 表单支持更深层对象数组嵌套,减少 raw YAML 回退
- 边界:不改变 Runtime / Generator 已定义的 schema 形状契约
- 验证:优先补 JS 测试,其次再做真实 VS Code 宿主手工验证
- [ ] Tooling为复杂结构提供比“顶层标量 / 标量数组”更强的批量编辑能力
- 边界:只增强编辑体验,不反向要求 schema 扩展或新的生成类型形状
- 验证:记录可观察的编辑路径和回退路径,而不是在 active 入口堆叠 UI 细节
- [ ] Tooling在真实 VS Code 宿主中完成对象数组编辑与复杂 schema 的交互式手工验证
- 边界:作为发布前增强项,不阻塞共享关键字主线
- 验证:后续 batch 直接补记宿主验证结论与未覆盖场景
- [ ] Docs在相关接入文档里补齐“工具能力是辅助层不定义 Runtime 契约”的读者提示
- 边界:只写 reader-facing 接入 guidance不写批次、治理、风险台账
- 验证:确认文档用语聚焦接入路径、能力边界和回退方案
## 暂缓 ## 暂缓
- [ ] 不追求完整 JSON Schema 全量支持 - [ ] 不追求完整 JSON Schema 全量支持
- 原因:维护成本高,且容易造成 Runtime / Generator / Tooling 三端漂移 - 原因:维护成本高,且容易造成 Runtime / Generator / Tooling 三端漂移;像 `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前已明确排除
- [ ] 不优先做运行时可写配置 - [ ] 不优先做运行时可写配置
- 原因:当前系统定位仍然是静态内容只读查询 - 原因:当前系统定位仍然是静态内容只读查询
@ -75,7 +100,7 @@
1. 用 `GeneratedConfigCatalog` 继续补齐启动与诊断辅助 1. 用 `GeneratedConfigCatalog` 继续补齐启动与诊断辅助
2. 补一条比 `Architecture.OnInitialize()` 更正式的模块化接入建议 2. 补一条比 `Architecture.OnInitialize()` 更正式的模块化接入建议
当前状态:第 1 项和第 2 项已完成,`allOf` 与 object-focused `if` / `then` / `else` 也已补齐;下一步转到下一批仍不改变生成形状的组合关键字评估,或继续推进 VS Code 复杂编辑体验 当前状态:第 1 项和第 2 项已完成,`allOf` 与 object-focused `if` / `then` / `else` 也已补齐;下一步默认转到下一批仍不改变生成形状的组合关键字评估。若另开并行 batch再从本文件的 Tooling / Docs lane 接手
## 完成标准 ## 完成标准
@ -87,12 +112,16 @@
## 下次恢复点 ## 下次恢复点
- 在当前稳定 `format` 子集(`date``date-time``duration``email``time``uri``uuid`、object-focused `allOf` 与 object-focused `if` / `then` / `else` 之后,转到下一批仍不改变生成类型形状的关键字评估;仍然不要先回工具 UI - 在当前稳定 `format` 子集(`date``date-time``duration``email``time``uri``uuid`、object-focused `allOf` 与 object-focused `if` / `then` / `else` 之后,转到下一批仍不改变生成类型形状的关键字评估;仍然不要先回工具 UI
- `oneOf` / `anyOf` 已明确跳过;恢复时不要再把它们当作默认候选
- 恢复时优先检查: - 恢复时优先检查:
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs` - `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
- `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` - `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`
- `tools/gframework-config-tool/src/configValidation.js` - `tools/gframework-config-tool/src/configValidation.js`
- `tools/gframework-config-tool/src/extension.js` - `tools/gframework-config-tool/src/extension.js`
- `docs/zh-CN/game/config-system.md` - `docs/zh-CN/game/config-system.md`
- 若恢复的是 Tooling / Docs 并行 lane
- 先回看本文件的 `Tooling / Docs 并行 Lane`
- 只把结果摘要回填到 active tracking / trace避免把编辑器批次细节重新塞回默认入口
### 恢复块 ### 恢复块
@ -108,5 +137,6 @@
- 结果:通过 - 结果:通过
- 下一步: - 下一步:
1. 检查 `YamlConfigSchemaValidator.cs``SchemaConfigGenerator.cs``configValidation.js` 中当前已支持的关键字列表 1. 检查 `YamlConfigSchemaValidator.cs``SchemaConfigGenerator.cs``configValidation.js` 中当前已支持的关键字列表
2. 评估 `oneOf` / `anyOf` 是否存在可接受的 object-focused 子集 2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
3. 若结论否定,选择下一批共享解释关键字而不是先回工具 UI 3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
4. 若主线批次暂不动代码,可并行开启 Tooling / Docs lane但不要让其反向改写主线恢复点定义

View File

@ -11,22 +11,23 @@
- 当前阶段:`C# Runtime + Source Generator + Consumer DX` - 当前阶段:`C# Runtime + Source Generator + Consumer DX`
- 当前焦点: - 当前焦点:
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字 - 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
- 已完成 PR #262 的 CodeRabbit follow-up补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests - 已完成 PR #262 的 CodeRabbit follow-up补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema - 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
- 继续把 VS Code 工具能力视为非阻塞项,不让复杂 UI 编辑器需求反过来拖慢 C# 主线 - Tooling / Docs 后续改为非阻塞并行 laneactive 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
### 已知风险 ### 已知风险
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移 - 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
- 缓解措施:延续 object-focused / focused matcher 约束,只接受三端都能稳定解释且不需要属性合并的子集 - 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
- 工具链验证风险VS Code 与 CI / 发布管道验证覆盖不足 - 工具链验证风险VS Code 与 CI / 发布管道验证覆盖不足
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证 - 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
- PR review 信号漂移风险CodeRabbit 可能把建议折叠在 latest review body而不是 issue comments - PR review 信号漂移风险CodeRabbit 可能把建议折叠在 latest review body而不是 issue comments
- 缓解措施:`gframework-pr-review` 现已同时解析 latest review body并输出 declared / parsed 数量以便快速识别解析缺口 - 缓解措施:`gframework-pr-review` 现已同时解析 latest review body并输出 declared / parsed 数量以便快速识别解析缺口
- PR follow-up 残留风险PR `#262` 最新 review thread 仍有少量 open comments且 nitpick body 解析仍存在 declared / parsed 缺口 - PR follow-up 残留风险PR `#262` 最新 review thread 仍有少量 open comments且 nitpick body 解析仍存在 declared / parsed 缺口
- 缓解措施:先以 latest unresolved thread 为准逐条本地核验;已确认并补齐运行时诊断路径与 `else without if` 回归测试skill 现已补齐 `.py` nitpick 与 outside-diff comment 解析,剩余项只需等待本地修复推送后再复抓确认 - 缓解措施:先以 latest unresolved thread 为准逐条本地核验;已确认并补齐运行时诊断路径与 `else without if` 回归测试skill 现已补齐 `.py` nitpick 与 outside-diff comment 解析,剩余项只需等待本地修复推送后再复抓确认
- 非阻塞项回退风险:将 VS Code 功能标为非阻塞但导致主线回退的风险 - 并行 lane 漂移风险Tooling / Docs 作为并行项后,后续 batch 可能重新把治理说明写回 active 入口或 public docs
- 缓解措施:C# 主线补齐新关键字时仍需在 `configValidation.js``extension.js` 中同步落地,只是不让复杂表单控件阻塞发布 - 缓解措施:active tracking / trace 只保留恢复点、验证和 lane 指针reader-facing 文档只写接入信息,治理说明继续留在 `ai-plan/**`
## 当前状态 ## 当前状态
@ -35,8 +36,10 @@
- 已补齐一批共享 JSON Schema 子集,包括: - 已补齐一批共享 JSON Schema 子集,包括:
- `enum``const``not``pattern` - `enum``const``not``pattern`
- `format` 稳定子集:`date``date-time``duration``email``time``uri``uuid` - `format` 稳定子集:`date``date-time``duration``email``time``uri``uuid`
- `minItems`、`maxItems`、`exclusiveMinimum``exclusiveMaximum``multipleOf``uniqueItems` - `minLength`、`maxLength``minItems`、`maxItems`、`contains``minContains``maxContains`、`exclusiveMinimum``exclusiveMaximum``multipleOf``uniqueItems`
- `minProperties``maxProperties``dependentRequired``dependentSchemas``allOf`、object-focused `if` / `then` / `else` - `minProperties``maxProperties``dependentRequired``dependentSchemas``allOf`、object-focused `if` / `then` / `else`
- 已明确拒绝会改变生成类型形状的组合关键字:
- `oneOf``anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地: - `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
- 只允许 object 节点上的 object-typed inline schema - 只允许 object 节点上的 object-typed inline schema
- `if` 必填,且必须至少伴随 `then``else` 之一 - `if` 必填,且必须至少伴随 `then``else` 之一
@ -70,9 +73,7 @@
- 继续扩展“不会改变生成类型形状”的共享关键字支持 - 继续扩展“不会改变生成类型形状”的共享关键字支持
- 继续降低复杂 schema 与多配置域项目的接入成本 - 继续降低复杂 schema 与多配置域项目的接入成本
- 让 VS Code 表单支持更深层对象数组嵌套,减少 raw YAML 回退 - Tooling / Docs 并行 lane 仍需推进复杂表单、交互式宿主验证和后续接入文档,但这些事项不再阻塞当前恢复点
- 为复杂结构提供比“顶层标量 / 标量数组”更强的批量编辑能力
- 在真实 VS Code 宿主中完成对象数组编辑与复杂 schema 的交互式手工验证
## 活跃文档 ## 活跃文档
@ -84,15 +85,12 @@
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace - `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史 - active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
- `2026-04-20` 当前恢复点验证: - 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 262 --format json`:通过(`CodeRabbit outside-diff comments: 1 declared, 1 parsed``CodeRabbit nitpick comments: 2 declared, 2 parsed` - PR `#306` follow-up 摘要:已按 latest open review threads 补齐 Generator `anyOf` 对称回归、Tooling schema type 白名单、object-array 直系收集边界,以及 reader-facing docs 的显式 `additionalProperties: false` / adoption guidance 说明;细节和验证命令保留在 trace 的 `2026-04-30` 新增阶段记录中
- `bun run test``tools/gframework-config-tool`通过122 tests包含条件分支坏形状回归 - PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准active tracking 不再重复展开逐条命令历史
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`:通过
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderIfThenElseTests"`通过8 tests新增 `else without if` 运行时回归)
- `dotnet build GFramework.sln -c Release`:通过(存在仓库既有 analyzer warning无新增错误
## 下一步 ## 下一步
1. 提交并推送当前 PR `#262` follow-up 修复后,重新抓取一次 PR review确认 outside-diff comment 与 open thread 是否都已收口 1. 主线继续回到 `YamlConfigSchemaValidator.cs``SchemaConfigGenerator.cs``configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf`
2. 若 PR review 已收口,再回到 `GFramework.Game/Config/YamlConfigSchemaValidator.cs``GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``tools/gframework-config-tool/src/configValidation.js` 盘点下一批候选关键字 2. Tooling / Docs 若要并发推进,优先补 reader-facing 示例或采用路径,不再重复扩写能力边界说明
3. 优先判断 `oneOf` / `anyOf` 是否存在可接受的 object-focused 子集;若仍会引入生成类型形状漂移,就直接跳过 3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针

View File

@ -106,6 +106,128 @@
### 下一步 ### 下一步
1. 评估 `oneOf` / `anyOf` 是否值得继续沿用 object-focused 子集;若仍会造成生成形状漂移,就直接跳过 1. 跳过 `oneOf` / `anyOf`,优先筛选下一个仍不改变生成类型形状、且不需要属性合并或联合分支生成的共享关键字
2. 若继续扩共享关键字,先在 Runtime / Generator / Tooling 三端同时定义一致边界,再进入实现 2. 若继续扩共享关键字,先在 Runtime / Generator / Tooling 三端同时定义一致边界,再进入实现
3. 继续把 active 入口保持精简,只记录当前恢复点、验证与下一步 3. 继续把 active 入口保持精简,只记录当前恢复点、验证与下一步
## 2026-04-30
### 阶段组合关键字边界收口AI-FIRST-CONFIG-RP-003
- 已在 Runtime、Source Generator 与 VS Code Tooling 三端显式拒绝 `oneOf` / `anyOf`
- 本轮结论不是继续做 object-focused 子集,而是先收紧共享边界:
- `oneOf` / `anyOf` 更容易引入联合分支、属性合并或生成类型形状漂移
- 当前配置系统主线仍优先保证 `C# Runtime + Source Generator + Consumer DX` 的稳定契约
- 因此三端统一改为在 schema 解析 / 生成阶段直接失败,避免静默忽略同一份 schema
- active tracking 也已同步更新,不再把 `oneOf` / `anyOf` 作为下一批默认候选
### 验证
- 2026-04-30`bun run test``tools/gframework-config-tool`
- 目标:验证工具端会拒绝 `oneOf`
- 2026-04-30`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
- 目标:验证生成器新增 `GF_ConfigSchema_015`
- 2026-04-30`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderAllOfTests"`
- 目标:验证运行时会拒绝对象节点上的 `oneOf`
### 下一步
1. 若本轮定向验证通过,继续盘点下一批真正低风险、且不改变生成类型形状的共享关键字
2. 不再重复评估 `oneOf` / `anyOf` 的 object-focused 子集,除非未来主线明确接受联合形状生成
3. 若后续关键字需要新诊断编号或文档边界说明,继续保持 Runtime / Generator / Tooling 同步收口
### 阶段Tooling lane 收口整理AI-FIRST-CONFIG-RP-003
- 已把 Tooling / Docs 后续动作从 active 入口的主线叙述中剥离,改成 backlog 文件里的非阻塞并行 lane
- 当前 active tracking / trace 只继续承担三件事:
- 给 `boot` 提供当前恢复点
- 记录最近一次验证或计划性验证占位
- 指向真正承载并行批次细节的 backlog 文件
- 本轮不新增代码范围、测试范围或文档范围,只整理 public `ai-plan/**` 的恢复入口表达,避免把治理噪音带回 reader-facing docs
### 关键决定
- `C# Runtime + Source Generator + Consumer DX` 仍是默认恢复主线
- Tooling / Docs 可以并发推进,但后续 batch 应直接以 `ai-first-config-system-csharp-experience-next.md` 为入口,而不是继续扩写 active tracking / trace
- public docs 后续只承接接入 guidance、能力边界和回退方式批次编排、lane 风险和治理说明继续留在 `ai-plan/**`
### 验证
- 2026-04-30`wc -l ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-csharp-experience-next.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`
- 结果:通过
- 备注:确认本轮仍把 active 入口控制在精简范围,并把 lane 细节下沉到 backlog 文件
### 下一步
1. 若继续做主线代码批次,直接回到共享关键字盘点,不让 Tooling / Docs 成为阻塞条件
2. 若另开 Tooling / Docs batch先读取 `ai-first-config-system-csharp-experience-next.md` 的并行 lane再把结果摘要写回 active tracking / trace
3. 继续保持 active 入口精简,不在默认恢复文件中追加 UI 细节、治理台账或面向读者的文档草稿
### 阶段Tooling / Docs reader-facing 边界补齐AI-FIRST-CONFIG-RP-003
- 已在 `config-tool.md``config-system.md``tools/gframework-config-tool/README.md` 明确 reader-facing 能力边界
- 本轮重点不是新增能力,而是把当前分支已经落地的结论写清楚:
- `contains` / `minContains` / `maxContains`
- `dependentRequired``dependentSchemas``allOf`
- object-focused `if` / `then` / `else`
- `additionalProperties: false`
- `oneOf` / `anyOf` rejection
- 同时补充了两个采用原则:
- VS Code 工具是辅助层,不定义 Runtime 契约
- 复杂 shape 或超出共享子集的 schema应回退到 raw YAML 与 schema 文件本体处理
### 验证
- 2026-04-30`git diff --check -- docs/zh-CN/game/config-tool.md docs/zh-CN/game/config-system.md tools/gframework-config-tool/README.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`
- 结果:通过
### 下一步
1. Tooling / Docs 后续若继续推进,优先补真实采用示例,而不是重复扩写边界清单
2. 主线代码批次继续以 Runtime / Generator / Tooling 三端共享关键字收口为中心
### 阶段Tooling parser 坏形状拒绝收紧AI-FIRST-CONFIG-RP-003
- 已在 `tools/gframework-config-tool/src/configValidation.js` 收紧工具侧 schema parser 边界
- 本轮不是扩 JSON Schema 能力,而是避免工具侧比 Runtime / Generator 更宽松:
- `additionalProperties` 现在只接受 `false`
- 数组 `items` 必须是 object-shaped 且显式带 `type`
- 数组 `contains` 若声明,也必须是 object-shaped 且显式带 `type`
- 这样 tuple-array `items: []`、缺失 `type``contains` 子 schema以及其他会误导用户以为“工具支持但运行时不支持”的坏形状会在工具解析阶段直接失败
### 验证
- 2026-04-30`bun run test``tools/gframework-config-tool`
- 结果:通过
- 备注:新增 JS 回归覆盖 `additionalProperties`、tuple-array `items` 与缺失 `type``contains`
### 下一步
1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状
2. 若继续做 Tooling lane优先补 reader-facing 示例或采用路径,而不是继续堆积边界清单
### 阶段PR #306 open threads 收口AI-FIRST-CONFIG-RP-003
- 已重新抓取 PR `#306` 的 latest open review threads并按“本地仍成立 / 已被当前分支吸收”重新核验
- 本轮收口重点不是继续扩能力,而是把 open threads 中仍成立的三类问题一次性补齐:
- Generator补齐 `GF_ConfigSchema_015``anyOf` 对称负例,避免组合关键字只覆盖 `oneOf`
- Tooling拒绝未知显式 `type`、收窄 object-array 只遍历当前 editor 直属 items、统一 `contains` hint 文案
- Docs`additionalProperties: false` 的“必须显式设置为 false”写清并为工具补最小接入示例、迁移提示与更准确的 raw YAML 回退条件
- 本轮同时更新了 JS / .NET 回归测试与 active tracking避免只修 review comment 不保留恢复点
### 验证
- 2026-04-30`bun run test``tools/gframework-config-tool`
- 结果通过132 tests
- 备注:新增未知 schema `type` 拒绝、嵌套 object-array 不串层,以及 `contains` hint 文案回归
- 2026-04-30`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
- 结果通过54 tests
- 备注:补齐 `Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf`
- 2026-04-30`git diff --check`
- 结果:通过
- 备注:本轮代码与文档改动未引入空白或冲突标记问题
### 下一步
1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复
2. 若还有残留 open threads优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理

View File

@ -25,6 +25,25 @@ description: GFramework.Game.Abstractions 的契约边界、包关系与源码
- 运行时实现:`GFramework.Game` - 运行时实现:`GFramework.Game`
- 底层基础契约:`GFramework.Core.Abstractions` - 底层基础契约:`GFramework.Core.Abstractions`
## 配置契约的采用边界
如果你只依赖 `GFramework.Game.Abstractions`,需要额外记住一件事:这里的 `Config/` 只定义“如何注册与访问配置表”的读取契约,不定义
AI-First 配置工作流的完整实现边界。
与配置相关的实际采用路径仍然要回到 `GFramework.Game`
- `YamlConfigLoader``GameConfigBootstrap``GameConfigModule` 等实现都在 `GFramework.Game`
- `GFramework.Game.SourceGenerators` 生成的配置类型,服务的是与 Runtime 对齐的共享 schema 子集
- 共享子集之外的复杂 schema 设计,不会因为你只依赖 abstractions 就自动获得额外支持
这意味着,如果你的 schema 依赖下面这些能力,就不能只停留在 abstractions 视角理解配置契约:
- `oneOf``anyOf` 这类复杂组合关键字
- 非 `false``additionalProperties`
- 其他会引入开放对象形状、联合分支或属性合并漂移的 schema 设计
这些边界由 `GFramework.Game` 与 [配置系统](../game/config-system.md) 负责说明和落地;`GFramework.Game.Abstractions` 本身不重新定义它们。
## 契约地图 ## 契约地图
| 契约族 | 作用 | | 契约族 | 作用 |
@ -105,6 +124,7 @@ public sealed class ContinueGameCommandHandler
- 使用 `SettingsModel<TRepository>``SettingsSystem``SaveRepository<TSaveData>` 等默认实现 - 使用 `SettingsModel<TRepository>``SettingsSystem``SaveRepository<TSaveData>` 等默认实现
- 使用 `YamlConfigLoader``GameConfigBootstrap``GameConfigModule` - 使用 `YamlConfigLoader``GameConfigBootstrap``GameConfigModule`
- 继承 `SceneRouterBase``UiRouterBase` 或默认转场处理器基类 - 继承 `SceneRouterBase``UiRouterBase` 或默认转场处理器基类
- 需要确认 AI-First 配置工作流当前支持的共享 schema 子集,以及 `oneOf` / `anyOf`、非 `false` `additionalProperties` 等不在采用路径内的边界
## 阅读顺序 ## 阅读顺序
@ -119,3 +139,5 @@ public sealed class ContinueGameCommandHandler
4. 需要统一入口时,回到: 4. 需要统一入口时,回到:
- [Game 模块总览](../game/index.md) - [Game 模块总览](../game/index.md)
- [入门指南](../getting-started/index.md) - [入门指南](../getting-started/index.md)
如果你的关注点是配置契约,请把 [配置系统](../game/config-system.md) 当作下一跳,而不是停留在 abstractions 页面对支持边界做推断。

View File

@ -13,6 +13,8 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
2. 再进专题页确认安装、生命周期和推荐接线方式 2. 再进专题页确认安装、生命周期和推荐接线方式
3. 最后回到源码中的 XML 文档核对具体契约 3. 最后回到源码中的 XML 文档核对具体契约
如果你在阅读 AI-First 配置工作流相关 API先把 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 视为正式契约入口,再把 `VS Code` 配置工具视为辅助层。当前默认采用路径围绕共享 schema 子集展开,其中 `additionalProperties: false` 表示闭合对象边界(需显式设置为 `false``oneOf` / `anyOf` 在 Runtime / Generator / Tooling 层面会被直接拒绝。更复杂的 shape 应回到 raw YAML 与 schema 设计本体处理。
## 阅读顺序 ## 阅读顺序
### 安装与选包入口 ### 安装与选包入口
@ -30,7 +32,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 | | `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract | | `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 | | `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约;其中 AI-First 配置工作流的正式支持边界以 Runtime + Generator 共享 schema 子集为准 |
| `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 | | `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 | | `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
@ -60,6 +62,9 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
- 最佳实践:[最佳实践](../best-practices/index.md) - 最佳实践:[最佳实践](../best-practices/index.md)
- 故障排查:[故障排查](../troubleshooting.md) - 故障排查:[故障排查](../troubleshooting.md)
如果你阅读的是 AI-First 配置相关 API请直接把 [配置系统](../game/config-system.md) 视为边界说明页:
`additionalProperties: false``oneOf` / `anyOf` rejection 这类采用约束不会由 VS Code 工具或 abstractions 页面单独改写。
## 共享支撑层怎么看 ## 共享支撑层怎么看
- `GFramework.Core.SourceGenerators.Abstractions` - `GFramework.Core.SourceGenerators.Abstractions`

View File

@ -17,10 +17,20 @@ description: 说明 GFramework.Game 配置系统的定位、目录约定、生
- JSON Schema 作为结构描述 - JSON Schema 作为结构描述
- 一对象一文件的目录组织 - 一对象一文件的目录组织
- 运行时只读查询 - 运行时只读查询
- Runtime / Generator / Tooling 共享支持 `enum``const``not``minimum``maximum``exclusiveMinimum``exclusiveMaximum``multipleOf``minLength``maxLength``pattern``format`(当前稳定子集:`date``date-time``duration``email``time``uri``uuid`)、`minItems``maxItems``uniqueItems``contains``minContains``maxContains``minProperties``maxProperties``dependentRequired``dependentSchemas``allOf``if` / `then` / `else` - Runtime / Generator / Tooling 共享支持 `enum``const``not``minimum``maximum``exclusiveMinimum``exclusiveMaximum``multipleOf``minLength``maxLength``pattern``format`(当前稳定子集:`date``date-time``duration``email``time``uri``uuid`)、`minItems``maxItems``uniqueItems``contains``minContains``maxContains``minProperties``maxProperties``dependentRequired``dependentSchemas``allOf`object-focused `if` / `then` / `else`,以及闭合对象边界 `additionalProperties: false`
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录 - Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口 - VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
## 编辑器能力与 Runtime 契约
`GFramework Config Tool` 是这套配置系统的辅助层,不单独定义 Runtime 契约。
- 哪些 schema 能被正式采用,以 `GFramework.Game` Runtime 与 Source Generator 的共享支持边界为准
- VS Code 插件负责把这些已落地的边界提前暴露成浏览、表单、校验和批量编辑体验
- 工具层的可视化入口比 Runtime 契约更保守时,应该回到 raw YAML 和 schema 本体继续编辑,而不是把“当前没做成表单”误解为“运行时允许自由扩展”
因此,判断某个关键字是否可用时,应该先看这里定义的共享契约,再把工具当作帮助你按这份契约工作的编辑器入口。
对应工具说明见:[VS Code 配置工具](./config-tool.md) 对应工具说明见:[VS Code 配置工具](./config-tool.md)
## 推荐目录结构 ## 推荐目录结构
@ -802,6 +812,14 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
- `dependentSchemas`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受“已声明 sibling 字段触发 object 子 schema”的形状不改变生成类型形状并按 focused constraint block 语义允许条件子 schema 未声明的额外同级字段继续存在 - `dependentSchemas`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受“已声明 sibling 字段触发 object 子 schema”的形状不改变生成类型形状并按 focused constraint block 语义允许条件子 schema 未声明的额外同级字段继续存在
- `allOf`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema 数组,并按 focused constraint block 语义把每个条目叠加到当前对象上,不做属性合并,也不改变生成类型形状 - `allOf`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema 数组,并按 focused constraint block 语义把每个条目叠加到当前对象上,不做属性合并,也不改变生成类型形状
- `if` / `then` / `else`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema`if` 必填且必须至少配合 `then``else` 之一使用,分支只能约束父对象已声明的字段,不做属性合并,也不改变生成类型形状;条件匹配本身沿用 `dependentSchemas` / `allOf` 的 focused matcher 语义,允许对象保留未在条件块中声明的额外同级字段 - `if` / `then` / `else`供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema`if` 必填且必须至少配合 `then``else` 之一使用,分支只能约束父对象已声明的字段,不做属性合并,也不改变生成类型形状;条件匹配本身沿用 `dependentSchemas` / `allOf` 的 focused matcher 语义,允许对象保留未在条件块中声明的额外同级字段
- `additionalProperties`:当前共享支持边界只接受 `additionalProperties: false`;它用于声明对象是闭合的,运行时、生成器和工具都会据此拒绝未声明字段。其他 `additionalProperties` 形态当前不属于共享支持子集,会在解析或生成阶段直接被拒绝
- `oneOf` / `anyOf`当前不属于共享支持子集Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字
如果你的 schema 需要超出这些边界的复杂 shape推荐采用下面的回退顺序
1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口
2. 再核对该 shape 是否仍符合这里列出的共享支持子集
3. 如果它依赖 `oneOf` / `anyOf`、非 `false``additionalProperties`、会向父对象注入新字段的 `allOf` / `dependentSchemas` / `if` 分支,或者更异构的深层数组结构,就应当把它视为当前版本之外的设计,而不是工具层遗漏的“隐藏能力”
`allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。 `allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。
@ -980,52 +998,10 @@ var hotReload = loader.EnableHotReload(
## VS Code 工具 ## VS Code 工具
完整采用说明见:[VS Code 配置工具](./config-tool.md)。 `GFramework Config Tool` 是这套配置系统的编辑器侧辅助入口,用来把 `config/``schemas/`、轻量校验、
表单预览和批量维护收敛到一条 VS Code 工作流里。
仓库中的 `tools/gframework-config-tool` 当前提供以下能力: 它不改变本页定义的运行时、生成器和 schema 语义边界,只负责把这些既有约束投射到编辑器采用路径中。
- 浏览 `config/` 目录 如果你要了解工作区约定、命令入口、表单与批量编辑边界、适用场景,以及何时应该回退到 raw YAML
- 打开 raw YAML 文件 完整说明见:[VS Code 配置工具](./config-tool.md)。
- 打开匹配的 schema 文件
- 根据 VS Code 当前界面语言在英文和简体中文之间切换主要工具界面文本
- 对嵌套对象中的必填字段、未知字段、基础标量类型、标量数组和对象数组元素做轻量校验
- 对嵌套对象字段、对象数组、顶层标量字段和顶层标量数组提供轻量表单入口
- 在表单中渲染已有 YAML 注释,并允许直接编辑字段级 YAML 注释
- 对带 `x-gframework-ref-table` 的字段提供引用 schema / 配置域 / 引用文件跳转入口
- 对空配置文件提供基于 schema 的示例 YAML 初始化入口
- 对同一配置域内的多份 YAML 文件执行批量字段更新
- 在表单入口中显示 `title / description / default / const / enum / x-gframework-ref-tableUI 中显示为 ref-table / multipleOf / pattern / format / uniqueItems / contains / minContains / maxContains / minProperties / maxProperties / dependentRequired / dependentSchemas / allOf / if / then / else` 元数据;批量编辑入口当前只暴露顶层可批量改写字段所需的基础信息
当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。
对象数组编辑器当前支持:
- 新增和删除对象项
- 编辑对象项中的标量字段
- 编辑对象项中的标量数组
- 编辑对象项中的嵌套对象字段
如果对象数组项内部继续包含对象数组,当前仍建议回退到 raw YAML 完成。
当前批量编辑入口仍刻意限制在“同域文件统一改动顶层标量字段和顶层标量数组”,避免复杂结构批量写回时破坏人工维护的 YAML 排版。
## 适用范围
当前这套工具更适合已经定义好 schema、需要校验、轻量表单和批量改写能力的内容维护场景尤其适合由开发者或技术策划主导的游戏项目配置工作流。
以下场景目前仍建议保留 raw YAML 编辑,或由项目补充专用工具:
- 需要更完整的 JSON Schema 支持
- 需要在 VS Code 中安全编辑更深层对象数组嵌套
- 需要覆盖更复杂的数组结构和更深层 schema 关键字
## 工具形态建议
对当前仓库已经落地的工作流而言,`VS Code Extension` 形态已经可以覆盖 schema 校验、轻量表单、批量编辑和 raw YAML 回退这条采用路径。
如果你的团队出现以下需求,再评估独立 `Config Studio` 会更合适:
- 配置维护主要由非开发角色承担,希望进一步降低 VS Code 的安装和使用门槛
- 需要更重的表格视图、跨表可视化关系编辑、复杂审批流或离线发布流程
- 插件形态已经明显受限于 VS Code Webview / Extension API而不是 schema 与工作流本身
- 已经沉淀出稳定的 schema 元数据约定,足以支撑单独工具的长期维护

View File

@ -68,6 +68,138 @@ GameProject/
如果你更关心“当前 schema 和 YAML 是否仍一致”,优先使用全量校验;如果你只是定位单个字段或注释,优先使用 如果你更关心“当前 schema 和 YAML 是否仍一致”,优先使用全量校验;如果你只是定位单个字段或注释,优先使用
Explorer + 表单预览。 Explorer + 表单预览。
### 当前能力范围
仓库中的 `tools/gframework-config-tool` 当前提供以下能力:
- 浏览 `config/` 目录
- 打开 raw YAML 文件
- 打开匹配的 schema 文件
- 根据 VS Code 当前界面语言在英文和简体中文之间切换主要工具界面文本
- 对嵌套对象中的必填字段、未知字段、基础标量类型、标量数组和对象数组元素做轻量校验
- 对嵌套对象字段、对象数组、顶层标量字段和顶层标量数组提供轻量表单入口
- 在表单中渲染已有 YAML 注释,并允许直接编辑字段级 YAML 注释
- 对带 `x-gframework-ref-table` 的字段提供引用 schema / 配置域 / 引用文件跳转入口
- 对空配置文件提供基于 schema 的示例 YAML 初始化入口
- 对同一配置域内的多份 YAML 文件执行批量字段更新
- 在表单入口中显示 `title / description / default / const / enum / x-gframework-ref-tableUI 中显示为 ref-table / multipleOf / pattern / format / uniqueItems / contains / minContains / maxContains / minProperties / maxProperties / dependentRequired / dependentSchemas / allOf / if / then / else` 元数据;批量编辑入口当前只暴露顶层可批量改写字段所需的基础信息
- 对 `additionalProperties: false` 提供闭合对象边界校验,并在遇到 `oneOf` / `anyOf` 或其他当前未收口的组合形状时明确提示该 schema 不属于当前工具支持子集
当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。
对象数组编辑器当前支持:
- 新增和删除对象项
- 编辑对象项中的标量字段
- 编辑对象项中的标量数组
- 编辑对象项中的嵌套对象字段
- 编辑对象项内部继续嵌套的对象数组,只要这些内层对象数组项仍然由对象、标量字段、标量数组和嵌套对象组成
如果对象数组中混入了标量项,或者更深层结构超出当前 schema 子集,表单入口会明确提示该路径需要回退到 raw YAML。
当前批量编辑入口仍刻意限制在“同域文件统一改动顶层标量字段和顶层标量数组”,避免复杂结构批量写回时破坏人工维护的 YAML 排版。
### 工具边界与 Runtime 契约
这个扩展是编辑器侧的辅助层,不定义 `GFramework.Game` 的 Runtime 契约。
- Runtime / Source Generator 是否接受某份 schema决定了它是否属于当前配置系统的正式支持范围
- 工具里的表单、hint、校验和批量编辑只是把这套已落地契约搬到 VS Code 中帮助你更快发现问题
- 如果工具界面暂时没有把某个 shape 做成可视化编辑入口,不代表 Runtime 会自动接受更宽松的 schema同样如果 Runtime / Generator 已明确拒绝某类关键字,工具也不会把它包装成可继续编辑的“可用能力”
日常采用时,建议把它理解为“优先用工具加速已支持子集的维护;遇到边界时立刻回到 schema + raw YAML 本体确认”。
### 最小接入示例与兼容 / 迁移说明
项目里至少需要准备三类内容:
- `config/<domain>/*.yaml`:实际配置文件
- `schemas/<domain>.schema.json`:与该配置域对应的 schema
- VS Code 工作区里的 `GFramework Config Tool` 扩展,以及与 schema 保持一致的 `x-gframework-ref-table` 引用约定
最小目录可以从下面这个形态起步:
```text
GameProject/
├─ config/
│ └─ monster/
│ └─ slime.yaml
└─ schemas/
└─ monster.schema.json
```
最小 schema 示例:
```json
{
"type": "object",
"additionalProperties": false,
"required": ["id", "name", "rarity", "dropItems"],
"properties": {
"id": {
"type": "integer",
"title": "Monster Id",
"description": "Primary monster key.",
"default": 1
},
"name": {
"type": "string",
"title": "Display Name",
"minLength": 1
},
"rarity": {
"type": "string",
"enum": ["common", "elite", "boss"],
"default": "common"
},
"spawnTime": {
"type": "string",
"format": "time"
},
"dropItems": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
}
},
"rewardTableId": {
"type": "string",
"x-gframework-ref-table": "reward-table"
}
}
}
```
对应的 YAML 初始文件可以保持很小:
```yaml
id: 1
name: Slime
rarity: common
spawnTime: 08:30:00Z
dropItems:
- potion
rewardTableId: starter-reward
```
推荐接入顺序:
1. 在 VS Code 中打开包含 `config/``schemas/` 的工作区
2. 如果目录不是默认值,先设置 `gframeworkConfig.configPath``gframeworkConfig.schemasPath`
3. 通过 Explorer 打开目标 YAML 或 schema先跑一次全量校验
4. 对空 YAML 使用“基于 schema 的示例 YAML 初始化”,或直接从 raw YAML 开始录入
5. 需要统一改同域顶层标量字段时,再进入批量编辑
迁移自纯 raw YAML 工作流时,至少先检查下面几件事:
- `additionalProperties` 是否显式设置为 `false`;省略或 `true` 不属于当前共享支持子集
- schema 是否依赖 `oneOf` / `anyOf`;这些组合关键字会被 Runtime / Generator / Tooling 直接拒绝
- 对象数组里是否混入标量项,或是否存在更深、更异构的数组结构
- Runtime / Source Generator 是否已经接受这份 schema而不是只有编辑器里“暂时看起来能写”
当 schema 仍在共享支持子集内,但某段编辑路径已经超出轻量表单可视化边界时,优先回到 raw YAML不要把“工具暂时没有表单入口”误判成“运行时契约已放宽”。
## 推荐工作流 ## 推荐工作流
### 1. 浏览配置与 schema ### 1. 浏览配置与 schema
@ -94,6 +226,9 @@ Explorer + 表单预览。
- 顶层标量数组 - 顶层标量数组
- 嵌套对象字段 - 嵌套对象字段
- 对象数组 - 对象数组
- object-focused `if` / `then` / `else``dependentRequired``dependentSchemas``allOf`
- `contains` / `minContains` / `maxContains`
- `additionalProperties: false`
如果你进入更深层对象数组嵌套,当前更稳妥的做法通常是: 如果你进入更深层对象数组嵌套,当前更稳妥的做法通常是:
@ -101,6 +236,13 @@ Explorer + 表单预览。
2. 先看表单预览确认字段结构 2. 先看表单预览确认字段结构
3. 再回到 raw YAML 完成最终编辑 3. 再回到 raw YAML 完成最终编辑
以下 shape 目前也建议直接回退到 raw YAML并同时检查 schema 是否仍在当前共享支持子集内:
- 需要表达 `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字
- 需要 `additionalProperties` 的其他形态,而不是当前明确支持的 `additionalProperties: false`
- 需要在 `allOf``dependentSchemas``if` / `then` / `else` 中引入父对象未声明的新字段
- 需要比当前对象数组编辑器更深、更异构的数组结构
## 工作区设置 ## 工作区设置
当前公开设置只有两个: 当前公开设置只有两个:
@ -125,12 +267,33 @@ Explorer + 表单预览。
- 校验聚焦仓库当前支持的 schema 子集 - 校验聚焦仓库当前支持的 schema 子集
- 表单预览支持对象数组,但更深的嵌套对象数组仍可能需要回退到 raw YAML - 表单预览支持对象数组,但更深的嵌套对象数组仍可能需要回退到 raw YAML
- 批量编辑当前聚焦顶层标量和顶层标量数组字段 - 批量编辑当前聚焦顶层标量和顶层标量数组字段
- 共享约束里只支持闭合对象边界 `additionalProperties: false``oneOf` / `anyOf` 等改变生成形状的组合关键字会被明确拒绝
因此,最稳妥的理解方式是: 因此,最稳妥的理解方式是:
- 用它加速“浏览、定位、轻量校验、批量维护” - 用它加速“浏览、定位、轻量校验、批量维护”
- 不把它当成完整替代 YAML / schema 编辑的唯一入口 - 不把它当成完整替代 YAML / schema 编辑的唯一入口
## 适用范围
当前这套工具更适合已经定义好 schema、需要校验、轻量表单和批量改写能力的内容维护场景尤其适合由开发者或技术策划主导的游戏项目配置工作流。
以下场景目前仍建议保留 raw YAML 编辑,或由项目补充专用工具:
- 需要更完整的 JSON Schema 支持
- 需要覆盖更复杂的数组结构和更深层 schema 关键字
## 工具形态建议
对当前仓库已经落地的工作流而言,`VS Code Extension` 形态已经可以覆盖 schema 校验、轻量表单、批量编辑和 raw YAML 回退这条采用路径。
如果你的团队出现以下需求,再评估独立 `Config Studio` 会更合适:
- 配置维护主要由非开发角色承担,希望进一步降低 VS Code 的安装和使用门槛
- 需要更重的表格视图、跨表可视化关系编辑、复杂审批流或离线发布流程
- 插件形态已经明显受限于 VS Code Webview / Extension API而不是 schema 与工作流本身
- 已经沉淀出稳定的 schema 元数据约定,足以支撑单独工具的长期维护
## 继续阅读 ## 继续阅读
- [游戏内容配置系统](./config-system.md) - [游戏内容配置系统](./config-system.md)

View File

@ -18,6 +18,14 @@ description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说
如果先把这三类入口分开理解,后续接入时会清晰很多。 如果先把这三类入口分开理解,后续接入时会清晰很多。
## 与 AI-First 配置系统的边界
如果你是从 AI-First 配置工作流一路读到这里,需要先把“配置契约”和“运行时持久化”分开理解:
- 配置系统的 schema 支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
- `DataRepository``UnifiedSettingsDataRepository``SaveRepository<TSaveData>` 负责的是数据怎么落盘、怎么回读、怎么组织槽位,而不是放宽配置契约
- 如果配置设计依赖 `oneOf``anyOf`、非 `false``additionalProperties`(例如省略或 `true`),或其他更复杂的 schema shape应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体继续处理,而不是期待 repository 层自动接管这些边界
## 什么时候用哪个仓库 ## 什么时候用哪个仓库
### `DataRepository` ### `DataRepository`
@ -195,6 +203,7 @@ var saveConfiguration = new SaveConfiguration
- `UnifiedSettingsDataRepository` 不是通用万能仓库,它专门服务“多 section 聚合单文件”的场景 - `UnifiedSettingsDataRepository` 不是通用万能仓库,它专门服务“多 section 聚合单文件”的场景
- `SaveRepository<TSaveData>` 不负责业务层的 autosave 策略、云同步或存档选择 UI - `SaveRepository<TSaveData>` 不负责业务层的 autosave 策略、云同步或存档选择 UI
- `LoadAsync(...)` 返回新实例的行为适合默认启动路径;如果项目需要“缺档即报错”,应在业务层显式调用 `ExistsAsync(...)` - `LoadAsync(...)` 返回新实例的行为适合默认启动路径;如果项目需要“缺档即报错”,应在业务层显式调用 `ExistsAsync(...)`
- 如果 AI-First 配置系统里的 schema 已经超出 Runtime / Generator 共享子集repository 也不会替你放宽这些约束;这时应优先回到 [配置系统](./config-system.md) 与 raw YAML / schema 设计本身
## 继续阅读 ## 继续阅读

View File

@ -83,6 +83,13 @@ IStorage storage = new FileStorage("GameData", serializer);
- `GFramework.Game.SourceGenerators` - `GFramework.Game.SourceGenerators`
- `schemas/**/*.schema.json` + `config/**/*.yaml` - `schemas/**/*.schema.json` + `config/**/*.yaml`
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
shape优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
完整约定见: 完整约定见:
- [配置系统](./config-system.md) - [配置系统](./config-system.md)
@ -120,7 +127,7 @@ IStorage storage = new FileStorage("GameData", serializer);
- 运行时入口主要来自 `GFramework.Game` - 运行时入口主要来自 `GFramework.Game`
- 只依赖接口或拆分业务层时,补充 `GFramework.Game.Abstractions` - 只依赖接口或拆分业务层时,补充 `GFramework.Game.Abstractions`
- 需要静态内容配置类型和表包装生成时,再追加 `GFramework.Game.SourceGenerators` - 需要静态内容配置类型和表包装生成时,再追加 `GFramework.Game.SourceGenerators`
- 需要编辑器侧内容维护工作流时,再看 [VS Code 配置工具](./config-tool.md) - 需要编辑器侧内容维护工作流时,再看 [VS Code 配置工具](./config-tool.md),并把它视为共享契约之上的辅助层
## 对应模块入口 ## 对应模块入口

View File

@ -254,6 +254,21 @@ await sceneRouter.PopAsync();
- 项目提供 factory、root、资源映射和具体引擎装配 - 项目提供 factory、root、资源映射和具体引擎装配
- 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程 - 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程
## 配置系统边界提示
如果你的场景路由接线同时依赖 AI-First 配置系统,本页只负责说明场景宿主、路由和生命周期接法,不负责定义配置
schema 的正式支持边界。涉及 YAML 配置契约、组合关键字或编辑器辅助能力时,请回到
[Game 配置系统](./config-system.md) 作为正式说明页。
默认采用路径之外的场景包括:
- `oneOf` / `anyOf`
- 非 `false``additionalProperties`
- 依赖开放对象形状、形状合并或更复杂嵌套数组的 schema shape
这类复杂 shape 不应从场景接线页推断支持范围。`VS Code` 工具只是辅助编辑与预览层;如果遇到这些情况,应直接回到 raw YAML
和 schema 本体设计处理。
## 推荐阅读 ## 推荐阅读
1. [Game 模块总览](./index.md) 1. [Game 模块总览](./index.md)

View File

@ -148,11 +148,14 @@ var restored = serializer.Deserialize(json, data.GetType());
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./config-system.md)。 如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./config-system.md)。
如果你在配置系统里进一步碰到更复杂的 schema shape也要尽快回到配置系统主文档和 raw YAML / schema 本体继续设计。当前默认采用路径面向的是与 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 对齐的共享 schema 子集,不是任意 `JSON Schema` 的全量支持。
## 当前边界 ## 当前边界
- 当前公开默认实现只有 JSON没有内建 MessagePack、Binary 或 ProtoBuf 实现 - 当前公开默认实现只有 JSON没有内建 MessagePack、Binary 或 ProtoBuf 实现
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>``SaveRepository<TSaveData>` - `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>``SaveRepository<TSaveData>`
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters - 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
- 如果配置设计依赖 `oneOf``anyOf`、非 `false``additionalProperties`(例如省略或 `true`),或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层
## 继续阅读 ## 继续阅读

View File

@ -18,6 +18,14 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,
而不是只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切的模型。 而不是只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切的模型。
## 与 AI-First 配置系统的边界
如果你关注的是“配置内容最后怎么变成运行时设置”,这里也需要先分清职责:
- 配置 schema 的正式支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
- `UnifiedSettingsDataRepository``SettingsModel<TRepository>``SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf``anyOf`、非 `false``additionalProperties`(例如省略或 `true`)等配置边界
- 一旦配置设计开始依赖更复杂的 schema shape应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体处理,再决定设置层怎么消费这些结果
## 当前公开入口 ## 当前公开入口
### `ISettingsData` ### `ISettingsData`
@ -196,6 +204,7 @@ await settingsModel.SaveAllAsync();
- `SettingsModel<TRepository>` 负责数据生命周期,`SettingsSystem` 负责系统级调用入口;两者不要混成一个巨型服务 - `SettingsModel<TRepository>` 负责数据生命周期,`SettingsSystem` 负责系统级调用入口;两者不要混成一个巨型服务
- applicator 决定“怎么把数据应用到宿主”repository 决定“怎么保存数据”,两层职责不要互相侵入 - applicator 决定“怎么把数据应用到宿主”repository 决定“怎么保存数据”,两层职责不要互相侵入
- 设置迁移和存档迁移是两条不同管线;后者看 [数据与存档系统](./data.md) 里的 `SaveRepository<TSaveData>` - 设置迁移和存档迁移是两条不同管线;后者看 [数据与存档系统](./data.md) 里的 `SaveRepository<TSaveData>`
- 如果某个配置 shape 已经超出 Runtime / Generator 共享支持子集settings repository 和 `SettingsModel` 也不会替代配置系统去放宽它;应回到 [配置系统](./config-system.md) 与 raw YAML / schema 设计处理
## 继续阅读 ## 继续阅读

View File

@ -165,6 +165,8 @@ var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
- 业务层如果想保存设置,可继续阅读 [设置系统](./setting.md) - 业务层如果想保存设置,可继续阅读 [设置系统](./setting.md)
- 业务层如果只是需要底层存储实现,才直接依赖 `IStorage` - 业务层如果只是需要底层存储实现,才直接依赖 `IStorage`
如果你是在“配置系统最终把内容保存到哪里”这个角度读到这里,需要先把边界分开:`IStorage` 负责运行时持久化,不负责定义配置 schema 的支持范围。配置工作流里只要开始出现更复杂的 schema shape仍应先回到 [配置系统](./config-system.md) 和 raw YAML / schema 本体继续设计,再决定运行时是否需要额外存储落盘策略。
## 当前边界 ## 当前边界
- `FileStorage` 已经会通过注入的 `ISerializer` 自动序列化对象;默认接法不需要先手工 `Serialize(...)` 再把字符串写回 - `FileStorage` 已经会通过注入的 `ISerializer` 自动序列化对象;默认接法不需要先手工 `Serialize(...)` 再把字符串写回
@ -172,6 +174,7 @@ var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制 - `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁 - 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
- 原子写入只覆盖单文件替换,不等于多文件事务 - 原子写入只覆盖单文件替换,不等于多文件事务
- 如果配置建模依赖 `oneOf``anyOf`、非 `false``additionalProperties`(例如省略或 `true`),或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理
## 继续阅读 ## 继续阅读

View File

@ -324,6 +324,21 @@ uiRouter.Hide(modalHandle, UiLayer.Modal);
- 页面行为不仅有生命周期,还有输入、阻断、暂停契约 - 页面行为不仅有生命周期,还有输入、阻断、暂停契约
- router 是 UI 语义仲裁中心,项目输入层应主动接入它 - router 是 UI 语义仲裁中心,项目输入层应主动接入它
## 配置系统边界提示
如果你的 UI 宿主接线还会读取 AI-First 配置或 schema 驱动的页面数据,本页只说明 UI router、root、factory 与输入语义,
不负责定义配置系统的正式边界。凡是配置契约、组合关键字或工具辅助的支持范围,都应以
[Game 配置系统](./config-system.md) 为准。
默认采用路径之外的典型场景包括:
- `oneOf` / `anyOf`
- 非 `false``additionalProperties`
- 更复杂的 schema shape例如依赖开放对象形状、形状合并或更深层异构数组
`VS Code` 工具只是辅助层,不是配置边界定义页。遇到这些复杂 shape 时,应直接回到 raw YAML 和 schema 本体设计,
而不是从 UI 接线页推断是否“已经被工具支持”。
## 推荐阅读 ## 推荐阅读
1. [Game 模块总览](./index.md) 1. [Game 模块总览](./index.md)

View File

@ -70,12 +70,18 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
- Scene / UI / Routing 抽象与运行时 - Scene / UI / Routing 抽象与运行时
- 文件存储和序列化 - 文件存储和序列化
AI-First 配置工作流的正式契约以 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 的共享 schema 子集为准,`VS Code` 配置工具只负责辅助编辑与预览。默认采用路径当前以 `additionalProperties: false` 作为闭合对象边界,`oneOf` / `anyOf` 不在默认入口范围内;如果你的 schema shape 超出这组共享边界,优先回到 raw YAML 与 schema 设计本体继续建模。
对应文档: 对应文档:
- [Game 模块总览](../game/index.md) - [Game 模块总览](../game/index.md)
- [配置系统](../game/config-system.md) - [配置系统](../game/config-system.md)
- [安装配置](./installation.md) - [安装配置](./installation.md)
如果你准备采用 AI-First 配置工作流,建议尽早确认当前采用边界:对象闭合只收口到
`additionalProperties: false`,而 `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前不属于默认路径。
超过这组共享子集的复杂 schema shape应回到 raw YAML 与 schema 本体设计,而不是把差异理解成工具遗漏。
### Godot 项目接入 ### Godot 项目接入
继续叠加: 继续叠加:
@ -104,6 +110,8 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
- 为 CQRS handlers 生成注册表 - 为 CQRS handlers 生成注册表
- 生成 Godot 节点、场景和 UI 包装代码 - 生成 Godot 节点、场景和 UI 包装代码
如果你要处理的是更复杂的 schema 设计,而不是编译期生成代码本身,先确认它是否仍在 `Game` Runtime 与生成器共享子集内;超出时应优先调整 raw YAML / schema 方案,而不是假定编辑器入口少了某个开关。
继续阅读: 继续阅读:
- [源码生成器总览](../source-generators/index.md) - [源码生成器总览](../source-generators/index.md)

View File

@ -46,6 +46,8 @@ GFramework 采用模块化设计,不同包提供不同的功能:
- Arch ECS直接安装 `GeWuYou.GFramework.Ecs.Arch`;如果只想共享宿主循环或接口边界,可改为 `GeWuYou.GFramework.Ecs.Arch.Abstractions` - Arch ECS直接安装 `GeWuYou.GFramework.Ecs.Arch`;如果只想共享宿主循环或接口边界,可改为 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
如果你准备采用 AI-First 配置工作流,可以继续阅读 [游戏内容配置系统](../game/config-system.md) 与 [VS Code 配置工具](../game/config-tool.md)。 如果你准备采用 AI-First 配置工作流,可以继续阅读 [游戏内容配置系统](../game/config-system.md) 与 [VS Code 配置工具](../game/config-tool.md)。
接入时建议先按 Runtime + Source Generator 的共享 schema 子集设计配置模型,再把 `VS Code` 工具当作编辑辅助层来使用,而不是反过来以工具界面可编辑的 shape 作为正式契约。
尤其需要尽早知道两个当前边界:对象闭合只收口到 `additionalProperties: false`,而 `oneOf` / `anyOf` 会被直接拒绝。若配置模型超出这组共享边界,优先回到 raw YAML 与 schema 本体调整结构,而不是把差异理解成工具遗漏能力。
## 安装方式 ## 安装方式

View File

@ -116,6 +116,11 @@ architecture.RegisterUtility<ISaveRepository<GameSaveData>>(new SaveRepository<G
- 生成器表元数据 - 生成器表元数据
- 热重载可用性边界 - 热重载可用性边界
如果这里涉及 schema 采用边界,也应以 [Game 配置系统](../game/config-system.md) 为正式说明页,而不是把
`GodotFileStorage` 所在的宿主接线页理解成配置边界定义。默认采用路径之外的典型场景包括 `oneOf` / `anyOf`
`false``additionalProperties`,以及其他更复杂的 schema shape。`VS Code` 工具只是辅助编辑与预览层;
遇到这些情况时,应直接回到 raw YAML 和 schema 本体设计处理。
### 通用存储契约 ### 通用存储契约
宿主无关的 `IStorage``ScopedStorage``FileStorage` 和统一数据仓库语义,可继续阅读 宿主无关的 `IStorage``ScopedStorage``FileStorage` 和统一数据仓库语义,可继续阅读

View File

@ -38,8 +38,10 @@ features:
details: 在保持 Core / Game 运行时边界的前提下,补齐节点扩展、场景与 UI 接线、协程桥接和生成器辅助。 details: 在保持 Core / Game 运行时边界的前提下,补齐节点扩展、场景与 UI 接线、协程桥接和生成器辅助。
- title: 🧩 AI-First 配置工作流 - title: 🧩 AI-First 配置工作流
details: 通过 YAML + JSON Schema + Source Generator + VS Code 工具,把静态内容配置、校验、表单预览和批量编辑串成一条链路。 details: 通过 YAML + JSON Schema + Source Generator + VS Code 工具,把静态内容配置、校验、表单预览和批量编辑串成一条链路;正式契约来自 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 的共享 schema 子集,编辑器工具只负责辅助编辑与预览
- title: ⚡ Roslyn 源码生成器 - title: ⚡ Roslyn 源码生成器
details: 自动生成日志、上下文注入、配置类型、CQRS registry 和 Godot 辅助代码,并复用共享 diagnostics 约束生成行为。 details: 自动生成日志、上下文注入、配置类型、CQRS registry 和 Godot 辅助代码,并复用共享 diagnostics 约束生成行为。
--- ---
AI-First 配置工作流的默认采用路径以闭合对象模型为前提:当前使用 `additionalProperties: false` 作为对象边界,`oneOf` / `anyOf` 不在默认入口范围内。遇到超出共享 schema 子集的复杂 shape请直接回到 raw YAML 与 schema 设计本体处理,而不是把它当成编辑器能力遗漏。

View File

@ -42,6 +42,30 @@ GFramework 当前发布的生成器包是:
| `GFramework.Cqrs.SourceGenerators` | `GFramework.Cqrs` | | `GFramework.Cqrs.SourceGenerators` | `GFramework.Cqrs` |
| `GFramework.Godot.SourceGenerators` | `GFramework.Godot` | | `GFramework.Godot.SourceGenerators` | `GFramework.Godot` |
`GFramework.Game.SourceGenerators` 而言,这个“服务 `GFramework.Game`”的关系还包含一个采用前提:
- 它面向的是与 `GFramework.Game` Runtime 对齐的共享 schema 子集
- 它的目标是把当前运行时已经明确支持的配置契约生成成类型与表包装,而不是承诺任意 JSON Schema 都能直接生成
- 读者在评估配置工作流时,应始终把 [配置系统](../game/config-system.md) 视为实际采用边界的说明页
## Game 配置生成器的采用边界
如果你选择的是 `GFramework.Game.SourceGenerators`请先按“共享子集”来理解它而不是按“JSON Schema 全量实现”来理解它。
当前 reader-facing 的采用路径是:
- Runtime、Source Generator 与 Tooling 共同对齐一组共享关键字与对象形状约束
- 生成器只为这组已经收口的契约生成 C# 配置类型、表包装和相关注册入口
- 一旦 schema 超出这组共享边界,就应该回到 schema 本体与运行时专题页重新判断,而不是假设生成器会替你兜底
当前不属于默认采用路径的典型情况包括:
- `oneOf``anyOf` 这类会改变生成类型形状的组合关键字
- 非 `false``additionalProperties`(例如省略或 `true`
- 其他需要开放对象形状、联合分支或更自由属性合并的 schema 设计
这些场景当前不应被理解为“文档还没写到的隐藏支持”,而应被理解为:它们不在 `GFramework.Game` 现阶段共享配置契约内。
安装时通常保持生成器包与对应运行时包版本一致,并将生成器声明为: 安装时通常保持生成器包与对应运行时包版本一致,并将生成器声明为:
```xml ```xml
@ -85,6 +109,7 @@ GFramework 当前发布的生成器包是:
- 配置 schema 生成与运行时接法: - 配置 schema 生成与运行时接法:
- [配置系统](../game/config-system.md) - [配置系统](../game/config-system.md)
- 读者若需要确认共享 schema 子集、关闭对象边界或复杂组合关键字的限制,应以该页为准,而不是只从本页推断支持范围
- CQRS handler registry 生成器: - CQRS handler registry 生成器:
- [CQRS Handler Registry 生成器](./cqrs-handler-registry-generator.md) - [CQRS Handler Registry 生成器](./cqrs-handler-registry-generator.md)
- CQRS 模块族采用入口: - CQRS 模块族采用入口:

View File

@ -1,6 +1,6 @@
# GFramework Config Tool # GFramework Config Tool
VS Code extension for the GFramework AI-First config workflow. VS Code extension for browsing, validating, and lightweight editing in the GFramework AI-First config workflow.
## Purpose ## Purpose
@ -34,7 +34,7 @@ GameProject/
### Explorer View ### Explorer View
- Browse config files from the workspace `config/` directory - Browse config files from the first workspace folder's `config/` directory
- Group files by config domain - Group files by config domain
- Open matching schema files from `schemas/` - Open matching schema files from `schemas/`
@ -43,11 +43,12 @@ GameProject/
- Open raw YAML - Open raw YAML
- Open the matching schema - Open the matching schema
- Open a lightweight form preview - Open a lightweight form preview
- Revalidate saved config files automatically when they change
### Domain-Level Actions ### Domain-Level Actions
- Batch edit one config domain across multiple files for top-level scalar and scalar-array fields - Batch edit one config domain across multiple files for top-level scalar and scalar-array fields
- Run validation across the current workspace config surface - Validate all discovered config files from the explorer view
### Form / Validation Support ### Form / Validation Support
@ -56,6 +57,8 @@ GameProject/
- Jump from reference fields to the referenced schema, config domain, or direct config file when a reference value is - Jump from reference fields to the referenced schema, config domain, or direct config file when a reference value is
present present
- Initialize empty config files from schema-derived example YAML - Initialize empty config files from schema-derived example YAML
- Edit nested object fields recursively inside the form preview
- Edit arrays of objects in the form preview, including nested object fields inside each item
- Surface schema metadata such as `title`, `description`, `default`, `enum`, and `x-gframework-ref-table` in the - Surface schema metadata such as `title`, `description`, `default`, `enum`, and `x-gframework-ref-table` in the
lightweight editors lightweight editors
@ -69,6 +72,23 @@ The extension currently validates the repository's current schema subset:
- scalar arrays with scalar item type checks - scalar arrays with scalar item type checks
- arrays of objects whose items use the same supported subset recursively - arrays of objects whose items use the same supported subset recursively
- scalar `enum` constraints and scalar-array item `enum` constraints - scalar `enum` constraints and scalar-array item `enum` constraints
- scalar `const` constraints
- numeric range constraints such as `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`, and `multipleOf`
- string constraints such as `minLength`, `maxLength`, and `pattern`
- array constraints such as `minItems`, `maxItems`, `contains`, `minContains`, `maxContains`, and `uniqueItems`
- object constraints such as `minProperties`, `maxProperties`, `dependentRequired`, `dependentSchemas`, `allOf`, and
object-focused `if` / `then` / `else`
- closed-object validation through `additionalProperties: false`
- explicit rejection for unsupported combinators such as `oneOf` and `anyOf`, instead of silently ignoring them
## Contract Boundary
This extension is an editor-side helper. It does not define the runtime contract for `GFramework.Game`.
- The runtime and source generator remain the source of truth for which schema shapes are formally supported
- The VS Code experience mirrors that shared subset so unsupported shapes fail early during browsing or validation
- If a shape is too complex for the lightweight editors, fall back to raw YAML and the schema file first; do not assume
the runtime accepts a broader contract just because the editor has no custom form for it
## Workspace Settings ## Workspace Settings
@ -83,12 +103,29 @@ The extension currently validates the repository's current schema subset:
1. Install the extension in VS Code and open the workspace that contains your `config/` and `schemas/` directories. 1. Install the extension in VS Code and open the workspace that contains your `config/` and `schemas/` directories.
2. Keep the default workspace layout, or set `gframeworkConfig.configPath` and `gframeworkConfig.schemasPath` to your 2. Keep the default workspace layout, or set `gframeworkConfig.configPath` and `gframeworkConfig.schemasPath` to your
project-specific paths. project-specific paths relative to the first workspace folder.
3. Open the `GFramework Config` explorer view and select a config file or domain. 3. Open the `GFramework Config` explorer view and select a config file or domain.
4. Run validation first to confirm the current YAML files still match the supported schema subset. 4. Run validation first to confirm the current YAML files still match the supported schema subset.
5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits 5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits
when needed. when needed.
Minimal adoption checklist:
- Keep one workspace folder that contains both `config/` and `schemas/`
- Place each config domain under `config/<domain>/*.yaml`
- Place the matching schema at `schemas/<domain>.schema.json`
- Use `x-gframework-ref-table` only on fields that should link to another config domain or reference file
- Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it or setting
it to `true` is outside the supported subset
Use raw YAML directly when you need:
- deeper or more heterogeneous array shapes
- supported object rules such as `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` only when they
push the edit path beyond the lightweight form boundary
- `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML
- schema designs outside the current shared subset, including `oneOf`, `anyOf`, or non-`false` `additionalProperties`
## Documentation ## Documentation
- Chinese adoption guide: [Game 配置工具](../../docs/zh-CN/game/config-tool.md) - Chinese adoption guide: [Game 配置工具](../../docs/zh-CN/game/config-tool.md)
@ -98,8 +135,11 @@ The extension currently validates the repository's current schema subset:
- Multi-root workspaces use the first workspace folder - Multi-root workspaces use the first workspace folder
- Validation only covers the repository's current schema subset - Validation only covers the repository's current schema subset
- Form preview supports object-array editing, but nested object arrays inside array items still fall back to raw YAML - Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still
fall back to raw YAML
- Batch editing remains limited to top-level scalar fields and top-level scalar arrays - Batch editing remains limited to top-level scalar fields and top-level scalar arrays
- Closed-object support is limited to `additionalProperties: false`, and unsupported combinators such as `oneOf` /
`anyOf` are rejected on purpose
## Local Testing ## Local Testing

View File

@ -1,14 +1,14 @@
{ {
"extension.displayName": "GFramework Config Tool", "extension.displayName": "GFramework Config Tool",
"extension.description": "VS Code tooling for browsing, validating, and editing AI-First config files in GFramework projects.", "extension.description": "VS Code tooling for browsing, validating, form-preview editing, and domain batch updates for AI-First config files in GFramework projects.",
"view.gframeworkConfig.name": "GFramework Config", "view.gframeworkConfig.name": "GFramework Config",
"command.refresh.title": "GFramework Config: Refresh", "command.refresh.title": "GFramework Config: Refresh",
"command.openRaw.title": "GFramework Config: Open Raw File", "command.openRaw.title": "GFramework Config: Open Raw YAML",
"command.openSchema.title": "GFramework Config: Open Schema", "command.openSchema.title": "GFramework Config: Open Schema",
"command.openFormPreview.title": "GFramework Config: Open Form Preview", "command.openFormPreview.title": "GFramework Config: Open Form Preview",
"command.batchEditDomain.title": "GFramework Config: Batch Edit Domain", "command.batchEditDomain.title": "GFramework Config: Batch Edit Domain",
"command.validateAll.title": "GFramework Config: Validate All", "command.validateAll.title": "GFramework Config: Validate All",
"configuration.title": "GFramework Config", "configuration.title": "GFramework Config",
"configuration.configPath.description": "Relative path from the workspace root to the config directory.", "configuration.configPath.description": "Relative path from the first workspace folder to the config directory.",
"configuration.schemasPath.description": "Relative path from the workspace root to the schema directory." "configuration.schemasPath.description": "Relative path from the first workspace folder to the schema directory."
} }

View File

@ -1,14 +1,14 @@
{ {
"extension.displayName": "GFramework 配置工具", "extension.displayName": "GFramework 配置工具",
"extension.description": "为 GFramework 项目中的 AI-First 配置文件提供浏览、校验和编辑能力的 VS Code 扩展。", "extension.description": "为 GFramework 项目中的 AI-First 配置文件提供浏览、校验、表单预览编辑和配置域批量更新能力的 VS Code 扩展。",
"view.gframeworkConfig.name": "GFramework 配置", "view.gframeworkConfig.name": "GFramework 配置",
"command.refresh.title": "GFramework 配置:刷新", "command.refresh.title": "GFramework 配置:刷新",
"command.openRaw.title": "GFramework 配置:打开原始文件", "command.openRaw.title": "GFramework 配置:打开原始 YAML",
"command.openSchema.title": "GFramework 配置:打开 Schema", "command.openSchema.title": "GFramework 配置:打开 Schema",
"command.openFormPreview.title": "GFramework 配置:打开表单预览", "command.openFormPreview.title": "GFramework 配置:打开表单预览",
"command.batchEditDomain.title": "GFramework 配置:批量编辑配置域", "command.batchEditDomain.title": "GFramework 配置:批量编辑配置域",
"command.validateAll.title": "GFramework 配置:校验全部", "command.validateAll.title": "GFramework 配置:校验全部",
"configuration.title": "GFramework 配置", "configuration.title": "GFramework 配置",
"configuration.configPath.description": "从工作区目录到配置目录的相对路径。", "configuration.configPath.description": "从第一个工作区目录到配置目录的相对路径。",
"configuration.schemasPath.description": "从工作区目录到 Schema 目录的相对路径。" "configuration.schemasPath.description": "从第一个工作区目录到 Schema 目录的相对路径。"
} }

View File

@ -19,6 +19,7 @@ const DurationFormatPattern =
const TimeFormatPattern = const TimeFormatPattern =
/^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u; /^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
const SupportedStringFormats = new Set(["date", "date-time", "duration", "email", "time", "uri", "uuid"]); const SupportedStringFormats = new Set(["date", "date-time", "duration", "email", "time", "uri", "uuid"]);
const SupportedSchemaTypes = new Set(["object", "array", "string", "integer", "number", "boolean"]);
/** /**
* Compare two strings using the same UTF-16 code-unit ordering as C#'s * Compare two strings using the same UTF-16 code-unit ordering as C#'s
@ -1095,7 +1096,16 @@ function unquoteScalar(value) {
*/ */
function parseSchemaNode(rawNode, displayPath) { function parseSchemaNode(rawNode, displayPath) {
const value = rawNode && typeof rawNode === "object" ? rawNode : {}; const value = rawNode && typeof rawNode === "object" ? rawNode : {};
const type = typeof value.type === "string" ? value.type : "object"; const unsupportedCombinatorKeyword = getUnsupportedCombinatorKeywordName(value);
if (unsupportedCombinatorKeyword) {
throw new Error(
`Schema property '${displayPath}' declares unsupported combinator keyword '${unsupportedCombinatorKeyword}'. ` +
"The current config schema subset does not support combinators that can change generated type shape.");
}
validateUnsupportedOpenObjectKeyword(value, displayPath);
const type = resolveSupportedSchemaType(value.type, displayPath);
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath); const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath); const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath); const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath);
@ -1168,15 +1178,19 @@ function parseSchemaNode(rawNode, displayPath) {
} }
if (type === "array") { if (type === "array") {
const itemNode = parseSchemaNode(value.items || {}, joinArrayTemplatePath(displayPath)); const itemNode = parseRequiredArrayChildSchema(value.items, displayPath, "items");
const containsNode = value.contains && typeof value.contains === "object" const containsNode = value.contains === undefined
? parseSchemaNode(value.contains, joinArrayTemplatePath(displayPath)) ? undefined
: undefined; : parseOptionalArrayChildSchema(value.contains, displayPath, "contains");
if (!containsNode && if (!containsNode &&
(typeof metadata.minContains === "number" || typeof metadata.maxContains === "number")) { (typeof metadata.minContains === "number" || typeof metadata.maxContains === "number")) {
throw new Error(`Schema property '${displayPath}' declares 'minContains' or 'maxContains' without 'contains'.`); throw new Error(`Schema property '${displayPath}' declares 'minContains' or 'maxContains' without 'contains'.`);
} }
if (itemNode.type === "array") {
throw new Error(`Schema property '${displayPath}' uses unsupported nested array items.`);
}
if (containsNode && containsNode.type === "array") { if (containsNode && containsNode.type === "array") {
throw new Error(`Schema property '${displayPath}' uses unsupported nested array 'contains' schemas.`); throw new Error(`Schema property '${displayPath}' uses unsupported nested array 'contains' schemas.`);
} }
@ -1253,6 +1267,116 @@ function parseSchemaNode(rawNode, displayPath) {
}, value.const, displayPath), value.enum, displayPath); }, value.const, displayPath), value.enum, displayPath);
} }
/**
* Reject open-object keyword forms that would drift away from the Runtime and
* Source Generator contracts. The current shared subset keeps object fields
* closed and only accepts an explicit `additionalProperties: false` reminder.
*
* @param {Record<string, unknown>} schemaNode Raw schema object.
* @param {string} displayPath Logical property path.
*/
function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
if (!Object.prototype.hasOwnProperty.call(schemaNode, "additionalProperties")) {
return;
}
if (schemaNode.additionalProperties === false) {
return;
}
throw new Error(
`Schema property '${displayPath}' uses unsupported 'additionalProperties' metadata. ` +
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
}
/**
* Parse one required array child schema while keeping tooling errors aligned
* with the Runtime and Source Generator contracts.
*
* @param {unknown} rawChild Raw child schema node.
* @param {string} displayPath Logical parent array path.
* @param {"items" | "contains"} keywordName Child schema keyword.
* @returns {SchemaNode} Parsed child schema node.
*/
function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
return parseArrayChildSchema(rawChild, displayPath, keywordName);
}
/**
* Parse one optional array child schema when it is present.
*
* @param {unknown} rawChild Raw child schema node.
* @param {string} displayPath Logical parent array path.
* @param {"items" | "contains"} keywordName Child schema keyword.
* @returns {SchemaNode | undefined} Parsed child schema node.
*/
function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
return parseArrayChildSchema(rawChild, displayPath, keywordName);
}
/**
* Parse one array child schema only when it is object-shaped and explicitly
* typed. This avoids silently treating tuple arrays or malformed child
* schemas as empty object nodes.
*
* @param {unknown} rawChild Raw child schema node.
* @param {string} displayPath Logical parent array path.
* @param {"items" | "contains"} keywordName Child schema keyword.
* @returns {SchemaNode} Parsed child schema node.
*/
function parseArrayChildSchema(rawChild, displayPath, keywordName) {
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) {
throw new Error(
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
}
if (typeof rawChild.type !== "string") {
throw new Error(
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
}
return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath));
}
/**
* Resolve one schema type while rejecting explicit strings that the shared
* subset does not support.
*
* @param {unknown} rawType Raw schema type value.
* @param {string} displayPath Logical property path.
* @returns {"object" | "array" | "string" | "integer" | "number" | "boolean"} Supported schema type.
*/
function resolveSupportedSchemaType(rawType, displayPath) {
if (typeof rawType !== "string") {
return "object";
}
if (!SupportedSchemaTypes.has(rawType)) {
throw new Error(`Schema property '${displayPath}' declares unsupported type '${rawType}'.`);
}
return rawType;
}
/**
* Return the first combinator keyword that the current shared schema subset
* intentionally rejects to keep Runtime / Generator / Tooling behavior aligned.
*
* @param {Record<string, unknown>} schemaNode Raw schema object.
* @returns {string | undefined} Unsupported keyword name when present.
*/
function getUnsupportedCombinatorKeywordName(schemaNode) {
if (Object.prototype.hasOwnProperty.call(schemaNode, "oneOf")) {
return "oneOf";
}
if (Object.prototype.hasOwnProperty.call(schemaNode, "anyOf")) {
return "anyOf";
}
return undefined;
}
/** /**
* Parse one optional `not` sub-schema and keep path formatting aligned with * Parse one optional `not` sub-schema and keep path formatting aligned with
* the runtime/generator diagnostics. * the runtime/generator diagnostics.

View File

@ -39,7 +39,7 @@ function describeContainsSchema(containsSchema, localizer) {
/** /**
* Build localized contains-related hint lines for array fields. * Build localized contains-related hint lines for array fields.
* *
* @param {{contains?: {type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}, minContains?: number}} propertySchema Array property schema metadata. * @param {{contains?: {type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}, minContains?: number, maxContains?: number}} propertySchema Array property schema metadata.
* @param {{t: (key: string, params?: Record<string, string | number>) => string}} localizer Runtime localizer. * @param {{t: (key: string, params?: Record<string, string | number>) => string}} localizer Runtime localizer.
* @returns {string[]} Localized contains hint lines. * @returns {string[]} Localized contains hint lines.
*/ */
@ -51,7 +51,7 @@ function buildContainsHintLines(propertySchema, localizer) {
const effectiveMinContains = typeof propertySchema.minContains === "number" const effectiveMinContains = typeof propertySchema.minContains === "number"
? propertySchema.minContains ? propertySchema.minContains
: 1; : 1;
return [ const lines = [
localizer.t("webview.hint.contains", { localizer.t("webview.hint.contains", {
summary: describeContainsSchema(propertySchema.contains, localizer) summary: describeContainsSchema(propertySchema.contains, localizer)
}), }),
@ -59,6 +59,14 @@ function buildContainsHintLines(propertySchema, localizer) {
value: effectiveMinContains value: effectiveMinContains
}) })
]; ];
if (typeof propertySchema.maxContains === "number") {
lines.push(localizer.t("webview.hint.maxContains", {
value: propertySchema.maxContains
}));
}
return lines;
} }
module.exports = { module.exports = {

View File

@ -1,6 +1,43 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const vscode = require("vscode"); let vscode;
try {
vscode = require("vscode");
} catch {
// Tests load pure helpers from this module without the VS Code host.
vscode = {
env: {
language: "en"
},
EventEmitter: class EventEmitter {
constructor() {
this.event = () => undefined;
}
fire() {
}
},
TreeItem: class TreeItem {
constructor(label, collapsibleState) {
this.label = label;
this.collapsibleState = collapsibleState;
}
},
TreeItemCollapsibleState: {
None: 0,
Collapsed: 1,
Expanded: 2
},
Uri: {
joinPath() {
return undefined;
}
},
window: {},
workspace: {},
languages: {}
};
}
const { const {
applyFormUpdates, applyFormUpdates,
createSampleConfigYaml, createSampleConfigYaml,
@ -972,8 +1009,14 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
current = current[segment]; current = current[segment];
} }
} }
function getDirectObjectArrayItems(editor) {
const itemsHost = editor.querySelector(":scope > [data-object-array-items]");
return itemsHost
? Array.from(itemsHost.querySelectorAll(":scope > [data-object-array-item]"))
: [];
}
function renumberObjectArrayItems(editor) { function renumberObjectArrayItems(editor) {
const items = editor.querySelectorAll("[data-object-array-item]"); const items = getDirectObjectArrayItems(editor);
items.forEach((item, index) => { items.forEach((item, index) => {
const title = item.querySelector(".object-array-item-title"); const title = item.querySelector(".object-array-item-title");
if (title) { if (title) {
@ -981,6 +1024,52 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
} }
}); });
} }
function shouldIncludeNestedControl(control, ownerItem) {
return control.closest("[data-object-array-item]") === ownerItem;
}
function collectObjectArrayEditorItems(editor) {
const items = [];
for (const item of getDirectObjectArrayItems(editor)) {
items.push(collectObjectArrayItemValue(item));
}
return items;
}
function collectObjectArrayItemValue(item) {
const itemValue = {};
for (const control of item.querySelectorAll("[data-item-local-path]")) {
if (!shouldIncludeNestedControl(control, item)) {
continue;
}
setNestedObjectValue(itemValue, control.dataset.itemLocalPath, control.value);
}
for (const textarea of item.querySelectorAll("textarea[data-item-array-path]")) {
if (!shouldIncludeNestedControl(textarea, item)) {
continue;
}
setNestedObjectValue(
itemValue,
textarea.dataset.itemArrayPath,
parseArrayEditorValue(textarea.value));
}
for (const nestedEditor of item.querySelectorAll("[data-item-object-array-path]")) {
if (!shouldIncludeNestedControl(nestedEditor, item)) {
continue;
}
setNestedObjectValue(
itemValue,
nestedEditor.dataset.itemObjectArrayPath,
collectObjectArrayEditorItems(nestedEditor));
}
return itemValue;
}
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
const schemaButton = event.target.closest("[data-open-ref-schema]"); const schemaButton = event.target.closest("[data-open-ref-schema]");
if (schemaButton) { if (schemaButton) {
@ -1048,23 +1137,9 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
for (const textarea of document.querySelectorAll("textarea[data-comment-path]")) { for (const textarea of document.querySelectorAll("textarea[data-comment-path]")) {
comments[textarea.dataset.commentPath] = textarea.value; comments[textarea.dataset.commentPath] = textarea.value;
} }
for (const editor of document.querySelectorAll("[data-object-array-editor]")) { for (const editor of document.querySelectorAll("[data-object-array-editor][data-object-array-path]")) {
const path = editor.dataset.objectArrayPath; const path = editor.dataset.objectArrayPath;
const items = []; objectArrays[path] = collectObjectArrayEditorItems(editor);
for (const item of editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]")) {
const itemValue = {};
for (const control of item.querySelectorAll("[data-item-local-path]")) {
setNestedObjectValue(itemValue, control.dataset.itemLocalPath, control.value);
}
for (const textarea of item.querySelectorAll("textarea[data-item-array-path]")) {
setNestedObjectValue(
itemValue,
textarea.dataset.itemArrayPath,
parseArrayEditorValue(textarea.value));
}
items.push(itemValue);
}
objectArrays[path] = items;
} }
vscode.postMessage({ type: "save", scalars, arrays, objectArrays, comments }); vscode.postMessage({ type: "save", scalars, arrays, objectArrays, comments });
}); });
@ -1110,8 +1185,11 @@ function renderFormField(field) {
title: localizer.t("webview.objectArray.item"), title: localizer.t("webview.objectArray.item"),
fields: field.templateFields fields: field.templateFields
}); });
const pathAttribute = field.itemMode
? `data-item-object-array-path="${escapeHtml(field.path)}"`
: `data-object-array-path="${escapeHtml(field.path)}"`;
return ` return `
<div class="object-array depth-${field.depth}" data-object-array-editor data-object-array-path="${escapeHtml(field.path)}"> <div class="object-array depth-${field.depth}" data-object-array-editor ${pathAttribute}>
<div class="label">${escapeHtml(field.label)} ${field.required ? `<span class="badge">${escapeHtml(localizer.t("webview.badge.required"))}</span>` : ""}</div> <div class="label">${escapeHtml(field.label)} ${field.required ? `<span class="badge">${escapeHtml(localizer.t("webview.badge.required"))}</span>` : ""}</div>
<div class="meta-key">${escapeHtml(field.displayPath || field.path)}</div> <div class="meta-key">${escapeHtml(field.displayPath || field.path)}</div>
${renderYamlCommentBlock(field)} ${renderYamlCommentBlock(field)}
@ -1507,6 +1585,41 @@ function collectObjectArrayItemFields(schemaNode, yamlNode, localPath, displayPa
continue; continue;
} }
if (propertySchema.type === "array" &&
propertySchema.items &&
propertySchema.items.type === "object") {
const templateFields = [];
collectObjectArrayItemFields(
propertySchema.items,
undefined,
"",
joinArrayTemplatePath(itemDisplayPath),
depth + 1,
templateFields,
unsupported,
commentLookup);
fields.push({
kind: "objectArray",
path: itemLocalPath,
displayPath: itemDisplayPath,
label,
required: requiredSet.has(key),
depth,
schema: propertySchema,
itemMode: true,
comment: commentLookup[itemDisplayPath] || "",
items: buildObjectArrayItemModels(
propertySchema.items,
propertyValue,
itemDisplayPath,
depth + 1,
unsupported,
commentLookup),
templateFields
});
continue;
}
if (["string", "integer", "number", "boolean"].includes(propertySchema.type)) { if (["string", "integer", "number", "boolean"].includes(propertySchema.type)) {
fields.push({ fields.push({
kind: "scalar", kind: "scalar",
@ -2096,5 +2209,8 @@ function parseArrayFieldPayload(arrays) {
module.exports = { module.exports = {
activate, activate,
deactivate deactivate,
__test: {
buildFormModel
}
}; };

View File

@ -247,9 +247,9 @@ const zhCnMessages = {
"webview.hint.format": "格式:{value}", "webview.hint.format": "格式:{value}",
"webview.hint.minItems": "最少元素数:{value}", "webview.hint.minItems": "最少元素数:{value}",
"webview.hint.maxItems": "最多元素数:{value}", "webview.hint.maxItems": "最多元素数:{value}",
"webview.hint.contains": "Contains 约束{summary}", "webview.hint.contains": "contains 条件{summary}",
"webview.hint.minContains": "最少 contains 匹配数:{value}", "webview.hint.minContains": "最少匹配数:{value}",
"webview.hint.maxContains": "最多 contains 匹配数:{value}", "webview.hint.maxContains": "最多匹配数:{value}",
"webview.hint.uniqueItems": "元素必须唯一", "webview.hint.uniqueItems": "元素必须唯一",
"webview.hint.required": "必填字段:{properties}", "webview.hint.required": "必填字段:{properties}",
"webview.hint.itemMinimum": "元素最小值:{value}", "webview.hint.itemMinimum": "元素最小值:{value}",
@ -265,7 +265,7 @@ const zhCnMessages = {
"webview.hint.minProperties": "最少属性数:{value}", "webview.hint.minProperties": "最少属性数:{value}",
"webview.hint.maxProperties": "最多属性数:{value}", "webview.hint.maxProperties": "最多属性数:{value}",
"webview.hint.dependentRequired": "当 {trigger} 出现时:还必须声明 {dependencies}", "webview.hint.dependentRequired": "当 {trigger} 出现时:还必须声明 {dependencies}",
"webview.hint.dependentSchemas": "当 {trigger} 出现时:还必须满足 {schema}", "webview.hint.dependentSchemas": "当 {trigger} 出现时:还必须满足以下条件:{schema}",
"webview.hint.allOf": "还必须满足:{schema}", "webview.hint.allOf": "还必须满足:{schema}",
"webview.hint.ifThen": "当满足 {condition} 时:还必须满足 {schema}", "webview.hint.ifThen": "当满足 {condition} 时:还必须满足 {schema}",
"webview.hint.ifElse": "否则(当 {condition} 不匹配时):还必须满足 {schema}", "webview.hint.ifElse": "否则(当 {condition} 不匹配时):还必须满足 {schema}",
@ -277,7 +277,7 @@ const zhCnMessages = {
[ValidationMessageKeys.allOfViolation]: "对象“{displayPath}”必须满足全部 `allOf` schema第 {index} 项未匹配。", [ValidationMessageKeys.allOfViolation]: "对象“{displayPath}”必须满足全部 `allOf` schema第 {index} 项未匹配。",
[ValidationMessageKeys.constMismatch]: "属性“{displayPath}”必须匹配固定值 {value}。", [ValidationMessageKeys.constMismatch]: "属性“{displayPath}”必须匹配固定值 {value}。",
[ValidationMessageKeys.dependentRequiredViolation]: "属性“{triggerProperty}”存在时,必须同时声明属性“{displayPath}”。", [ValidationMessageKeys.dependentRequiredViolation]: "属性“{triggerProperty}”存在时,必须同时声明属性“{displayPath}”。",
[ValidationMessageKeys.dependentSchemasViolation]: "对象“{displayPath}”在属性“{triggerProperty}”存在时,必须满足对应的 dependent schema。", [ValidationMessageKeys.dependentSchemasViolation]: "对象“{displayPath}”在属性“{triggerProperty}”存在时,必须满足对应的依赖 schema。",
[ValidationMessageKeys.elseViolation]: "对象“{displayPath}”在内联 `if` 条件未命中时,必须满足对应的 `else` schema。", [ValidationMessageKeys.elseViolation]: "对象“{displayPath}”在内联 `if` 条件未命中时,必须满足对应的 `else` schema。",
[ValidationMessageKeys.exclusiveMaximumViolation]: "属性“{displayPath}”必须小于 {value}。", [ValidationMessageKeys.exclusiveMaximumViolation]: "属性“{displayPath}”必须小于 {value}。",
[ValidationMessageKeys.exclusiveMinimumViolation]: "属性“{displayPath}”必须大于 {value}。", [ValidationMessageKeys.exclusiveMinimumViolation]: "属性“{displayPath}”必须大于 {value}。",

View File

@ -1,6 +1,7 @@
const ValidationMessageKeys = Object.freeze({ const ValidationMessageKeys = Object.freeze({
allOfViolation: "validation.allOfViolation", allOfViolation: "validation.allOfViolation",
constMismatch: "validation.constMismatch", constMismatch: "validation.constMismatch",
dependentRequiredViolation: "validation.dependentRequiredViolation",
dependentSchemasViolation: "validation.dependentSchemasViolation", dependentSchemasViolation: "validation.dependentSchemasViolation",
elseViolation: "validation.elseViolation", elseViolation: "validation.elseViolation",
enumMismatch: "validation.enumMismatch", enumMismatch: "validation.enumMismatch",

View File

@ -1,5 +1,6 @@
const test = require("node:test"); const test = require("node:test");
const assert = require("node:assert/strict"); const assert = require("node:assert/strict");
const {__test: extensionTest} = require("../src/extension");
const { const {
applyFormUpdates, applyFormUpdates,
applyScalarUpdates, applyScalarUpdates,
@ -178,6 +179,67 @@ test("parseSchemaContent should preserve empty-string const raw and display meta
assert.equal(schema.properties.name.constDisplayValue, "\"\""); assert.equal(schema.properties.name.constDisplayValue, "\"\"");
}); });
test("parseSchemaContent should reject unsupported oneOf combinators", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"properties": {
"itemCount": { "type": "integer" }
},
"oneOf": [
{
"type": "object",
"required": ["itemCount"],
"properties": {
"itemCount": { "type": "integer" }
}
}
]
}
}
}
`),
/unsupported combinator keyword 'oneOf'/u);
});
test("parseSchemaContent should reject unsupported additionalProperties forms", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"additionalProperties": true,
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
`),
/unsupported 'additionalProperties' metadata/u);
});
test("parseSchemaContent should reject unsupported explicit schema types", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "bogus"
}
}
}
`),
/declares unsupported type 'bogus'/u);
});
test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => { test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => {
const schema = parseSchemaContent(` const schema = parseSchemaContent(`
{ {
@ -1516,6 +1578,45 @@ test("parseSchemaContent should reject nested-array contains schemas", () => {
/unsupported nested array 'contains' schemas/u); /unsupported nested array 'contains' schemas/u);
}); });
test("parseSchemaContent should reject array items without an explicit typed object schema", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"dropRates": {
"type": "array",
"items": [
{ "type": "integer" }
]
}
}
}
`),
/must declare 'items' as an object-valued schema with an explicit 'type'/u);
});
test("parseSchemaContent should reject contains without an explicit typed object schema", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"dropRates": {
"type": "array",
"contains": {
"const": 5
},
"items": {
"type": "integer"
}
}
}
}
`),
/must declare 'contains' as an object-valued schema with an explicit 'type'/u);
});
test("parseSchemaContent should reject minContains and maxContains without contains", () => { test("parseSchemaContent should reject minContains and maxContains without contains", () => {
assert.throws( assert.throws(
() => parseSchemaContent(` () => parseSchemaContent(`
@ -2498,6 +2599,166 @@ test("applyFormUpdates should rewrite object-array items from structured form pa
assert.match(updated, /^ monsterId: goblin$/mu); assert.match(updated, /^ monsterId: goblin$/mu);
}); });
test("buildFormModel should expose nested object-array editors inside object-array items", () => {
const schema = parseSchemaContent(`
{
"type": "object",
"properties": {
"phases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"wave": { "type": "integer" },
"spawns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"monsterId": { "type": "string" },
"tags": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
}
}
}
}
`);
const yaml = parseTopLevelYaml(`
phases:
-
wave: 1
spawns:
-
monsterId: slime
tags:
- starter
`);
const formModel = extensionTest.buildFormModel(schema, yaml, {});
const phasesField = formModel.fields.find((field) => field.path === "phases");
assert.ok(phasesField);
assert.equal(phasesField.kind, "objectArray");
assert.equal(phasesField.items.length, 1);
assert.deepEqual(formModel.unsupported, []);
const nestedSpawnField = phasesField.items[0].fields.find((field) => field.path === "spawns");
assert.ok(nestedSpawnField);
assert.equal(nestedSpawnField.kind, "objectArray");
assert.equal(nestedSpawnField.itemMode, true);
assert.equal(nestedSpawnField.items.length, 1);
const spawnMonsterField = nestedSpawnField.items[0].fields.find((field) => field.path === "monsterId");
assert.ok(spawnMonsterField);
assert.equal(spawnMonsterField.kind, "scalar");
const spawnTagsField = nestedSpawnField.items[0].fields.find((field) => field.path === "tags");
assert.ok(spawnTagsField);
assert.equal(spawnTagsField.kind, "array");
});
test("applyFormUpdates should rewrite nested object arrays from structured form payloads", () => {
const updated = applyFormUpdates(
[
"phases:",
" -",
" wave: 1"
].join("\n"),
{
objectArrays: {
phases: [
{
wave: "1",
spawns: [
{
monsterId: "slime",
tags: ["starter", "melee"],
reward: {
gold: "10"
}
},
{
monsterId: "goblin",
conditions: [
{
type: "night",
value: "true"
}
]
}
]
}
]
}
});
assert.match(updated, /^phases:$/mu);
assert.match(updated, /^ -$/mu);
assert.match(updated, /^ wave: 1$/mu);
assert.match(updated, /^ spawns:$/mu);
assert.match(updated, /^ -$/mu);
assert.match(updated, /^ monsterId: slime$/mu);
assert.match(updated, /^ tags:$/mu);
assert.match(updated, /^ - starter$/mu);
assert.match(updated, /^ - melee$/mu);
assert.match(updated, /^ reward:$/mu);
assert.match(updated, /^ gold: 10$/mu);
assert.match(updated, /^ monsterId: goblin$/mu);
assert.match(updated, /^ conditions:$/mu);
assert.match(updated, /^ -$/mu);
assert.match(updated, /^ type: night$/mu);
assert.match(updated, /^ value: true$/mu);
});
test("applyFormUpdates should not mix nested object-array items into the parent array", () => {
const updated = applyFormUpdates(
[
"phases:",
" -",
" wave: 1"
].join("\n"),
{
objectArrays: {
phases: [
{
wave: "1",
spawns: [
{
monsterId: "slime"
},
{
monsterId: "goblin"
}
]
},
{
wave: "2",
spawns: [
{
monsterId: "bat"
}
]
}
]
}
});
assert.equal((updated.match(/^ -$/gmu) || []).length, 2);
assert.equal((updated.match(/^ -$/gmu) || []).length, 3);
assert.doesNotMatch(updated, /^ monsterId: slime$/mu);
assert.doesNotMatch(updated, /^ monsterId: goblin$/mu);
assert.match(updated, /^ -$/mu);
assert.match(updated, /^ monsterId: slime$/mu);
assert.match(updated, /^ monsterId: goblin$/mu);
assert.match(updated, /^ monsterId: bat$/mu);
});
test("applyFormUpdates should clear object arrays when the form removes all items", () => { test("applyFormUpdates should clear object arrays when the form removes all items", () => {
const updated = applyFormUpdates( const updated = applyFormUpdates(
[ [

View File

@ -51,6 +51,7 @@ test("buildContainsHintLines should use explicit minContains when provided", ()
const lines = buildContainsHintLines( const lines = buildContainsHintLines(
{ {
minContains: 2, minContains: 2,
maxContains: 3,
contains: { contains: {
type: "string", type: "string",
constValue: "\"potion\"", constValue: "\"potion\"",
@ -62,7 +63,8 @@ test("buildContainsHintLines should use explicit minContains when provided", ()
assert.deepEqual(lines, [ assert.deepEqual(lines, [
"Contains: string, Const: \"potion\", Ref table: item", "Contains: string, Const: \"potion\", Ref table: item",
"Min contains: 2" "Min contains: 2",
"Max contains: 3"
]); ]);
}); });
@ -93,3 +95,24 @@ test("describeContainsSchema should format pattern-based contains schema in Chin
assert.equal(summary, "string, 正则模式:^potion-, 引用表item"); assert.equal(summary, "string, 正则模式:^potion-, 引用表item");
}); });
test("buildContainsHintLines should use updated Chinese contains hint wording", () => {
const localizer = createLocalizer("zh-cn");
const lines = buildContainsHintLines(
{
minContains: 1,
maxContains: 2,
contains: {
type: "string",
enumValues: ["potion", "elixir"]
}
},
localizer);
assert.deepEqual(lines, [
"contains 条件string, 允许值potion, elixir",
"最少匹配数1",
"最多匹配数2"
]);
});

View File

@ -68,6 +68,22 @@ test("createLocalizer should expose contains-count validation keys", () => {
assert.equal( assert.equal(
chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}), chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}),
"属性“dropRates”最多只能包含 1 个匹配 contains 条件的元素。"); "属性“dropRates”最多只能包含 1 个匹配 contains 条件的元素。");
assert.equal(
chineseLocalizer.t("webview.hint.contains", {summary: "object, Required: itemCount"}),
"contains 条件object, Required: itemCount");
});
test("createLocalizer should resolve dependentRequired through the explicit validation key", () => {
const localizer = createLocalizer("en");
assert.equal(ValidationMessageKeys.dependentRequiredViolation, "validation.dependentRequiredViolation");
assert.equal(
localizer.t(ValidationMessageKeys.dependentRequiredViolation, {
displayPath: "reward.itemCount",
triggerProperty: "reward.itemId"
}),
"Property 'reward.itemCount' is required when sibling property 'reward.itemId' is present.");
assert.equal(localizer.t("undefined"), "undefined");
}); });
test("createLocalizer should expose not validation keys", () => { test("createLocalizer should expose not validation keys", () => {
@ -132,7 +148,7 @@ test("createLocalizer should expose dependentSchemas validation keys", () => {
trigger: "reward.itemId", trigger: "reward.itemId",
schema: "object, 必填字段itemCount" schema: "object, 必填字段itemCount"
}), }),
"当 reward.itemId 出现时:还必须满足 object, 必填字段itemCount"); "当 reward.itemId 出现时:还必须满足以下条件:object, 必填字段itemCount");
assert.equal( assert.equal(
englishLocalizer.t(ValidationMessageKeys.dependentSchemasViolation, { englishLocalizer.t(ValidationMessageKeys.dependentSchemasViolation, {
displayPath: "reward", displayPath: "reward",
@ -144,7 +160,7 @@ test("createLocalizer should expose dependentSchemas validation keys", () => {
displayPath: "reward", displayPath: "reward",
triggerProperty: "reward.itemId" triggerProperty: "reward.itemId"
}), }),
"对象“reward”在属性“reward.itemId”存在时必须满足对应的 dependent schema。"); "对象“reward”在属性“reward.itemId”存在时必须满足对应的依赖 schema。");
}); });
test("createLocalizer should expose allOf validation keys", () => { test("createLocalizer should expose allOf validation keys", () => {