fix(game-config): 收紧开放对象关键字边界

- 修复 Runtime、Generator 与 Tooling 对 patternProperties、propertyNames、unevaluatedProperties 的静默接受风险

- 补充三端对称回归测试与 reader-facing 文档边界说明

- 更新 ai-plan 恢复点、验证记录与下一步指针
This commit is contained in:
gewuyou 2026-05-06 08:47:42 +08:00
parent a8c6c11e9e
commit cb6dd8a510
12 changed files with 319 additions and 24 deletions

View File

@ -884,7 +884,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
/// <summary> /// <summary>
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。 /// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的 /// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>。 /// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary> /// </summary>
/// <param name="filePath">Schema 文件路径。</param> /// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param> /// <param name="displayPath">逻辑字段路径。</param>
@ -959,12 +961,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
out Diagnostic? diagnostic) out Diagnostic? diagnostic)
{ {
diagnostic = null; diagnostic = null;
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement)) if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{ {
return true; return true;
} }
if (additionalPropertiesElement.ValueKind == JsonValueKind.False) if (string.Equals(keywordName, "additionalProperties", StringComparison.Ordinal) &&
element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind == JsonValueKind.False)
{ {
return true; return true;
} }
@ -974,8 +978,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
CreateFileLocation(filePath), CreateFileLocation(filePath),
Path.GetFileName(filePath), Path.GetFileName(filePath),
displayPath, displayPath,
"additionalProperties", keywordName,
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed."); "The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.");
return false; return false;
} }
@ -991,6 +995,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
null; null;
} }
/// <summary>
/// 返回当前节点声明的首个未支持开放对象关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
{
if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind != JsonValueKind.False)
{
return "additionalProperties";
}
return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
null;
}
/// <summary> /// <summary>
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。 /// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> / /// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /

View File

@ -81,6 +81,9 @@ GameProject/
- `oneOf` - `oneOf`
- `anyOf` - `anyOf`
- 非 `false``additionalProperties` - 非 `false``additionalProperties`
- `patternProperties`
- `propertyNames`
- `unevaluatedProperties`
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束 - 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束
遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。 遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。

View File

@ -448,6 +448,55 @@ public sealed class YamlConfigLoaderAllOfTests
}); });
} }
/// <summary>
/// 验证运行时会拒绝会重新打开对象形状的其他开放对象关键字。
/// </summary>
[TestCase("patternProperties", """
{
"^dynamic-": { "type": "integer" }
}
""")]
[TestCase("propertyNames", """
{
"pattern": "^[a-z]+$"
}
""")]
[TestCase("unevaluatedProperties", "false")]
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_OpenObject_Keyword(
string keywordName,
string keywordValueJson)
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
DefaultAllOfJson,
$$"""
"{{keywordName}}": {{keywordValueJson}}
"""));
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 '{keywordName}' metadata"));
Assert.That(exception.Message, Does.Contain("rejects keywords that reopen object shapes"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}
/// <summary> /// <summary>
/// 验证 allOf 条目只接受 object-typed schema。 /// 验证 allOf 条目只接受 object-typed schema。
/// </summary> /// </summary>

View File

@ -373,7 +373,9 @@ internal static partial class YamlConfigSchemaValidator
/// <summary> /// <summary>
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。 /// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的 /// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。 /// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary> /// </summary>
/// <param name="tableName">所属配置表名称。</param> /// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param> /// <param name="schemaPath">Schema 文件路径。</param>
@ -385,12 +387,14 @@ internal static partial class YamlConfigSchemaValidator
string propertyPath, string propertyPath,
JsonElement element) JsonElement element)
{ {
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement)) if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{ {
return; return;
} }
if (additionalPropertiesElement.ValueKind == JsonValueKind.False) if (string.Equals(keywordName, "additionalProperties", StringComparison.Ordinal) &&
element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind == JsonValueKind.False)
{ {
return; return;
} }
@ -398,8 +402,8 @@ internal static partial class YamlConfigSchemaValidator
throw ConfigLoadExceptionFactory.Create( throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported, ConfigLoadFailureKind.SchemaUnsupported,
tableName, tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported 'additionalProperties' metadata. " + $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.", "The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.",
schemaPath: schemaPath, schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath)); displayPath: GetDiagnosticPath(propertyPath));
} }
@ -416,6 +420,25 @@ internal static partial class YamlConfigSchemaValidator
null; null;
} }
/// <summary>
/// 返回当前节点声明的首个未支持开放对象关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
{
if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind != JsonValueKind.False)
{
return "additionalProperties";
}
return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
null;
}
/// <summary> /// <summary>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。 /// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </summary> /// </summary>

View File

@ -1965,6 +1965,58 @@ public class SchemaConfigGeneratorTests
}); });
} }
/// <summary>
/// 验证生成器会拒绝会重新打开对象形状的其他开放对象关键字。
/// </summary>
[TestCase("patternProperties", """
{
"^dynamic-": { "type": "integer" }
}
""")]
[TestCase("propertyNames", """
{
"pattern": "^[a-z]+$"
}
""")]
[TestCase("unevaluatedProperties", "false")]
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_OpenObject_Keyword(
string keywordName,
string keywordValueJson)
{
const string source = DummySource;
var schema = $$"""
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"{{keywordName}}": {{keywordValueJson}},
"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(keywordName));
Assert.That(diagnostic.GetMessage(), Does.Contain("rejects keywords that reopen object shapes"));
});
}
/// <summary> /// <summary>
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。 /// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
/// </summary> /// </summary>

View File

@ -12,6 +12,7 @@
- 当前焦点: - 当前焦点:
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字 - 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移 - 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
- 已把开放对象关键字边界收紧为只接受 `additionalProperties: false`,并在 Runtime / Generator / Tooling 三端显式拒绝 `patternProperties``propertyNames``unevaluatedProperties`
- 已完成 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
- Tooling / Docs 后续改为非阻塞并行 laneactive 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件 - Tooling / Docs 后续改为非阻塞并行 laneactive 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
@ -20,6 +21,8 @@
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移 - 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集 - 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
- 开放对象形状风险:如果某一端静默接受 `patternProperties``propertyNames``unevaluatedProperties` 等关键字,会重新打开对象形状并造成契约漂移
- 缓解措施:当前三端已统一把开放对象边界收紧为只接受 `additionalProperties: false`,其余开放对象关键字直接报错
- 工具链验证风险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
@ -40,6 +43,9 @@
- `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 三端直接报错,而不是静默忽略 - `oneOf``anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
- 已明确拒绝会重新打开对象形状的开放对象关键字:
- 当前只接受 `additionalProperties: false`
- `patternProperties``propertyNames``unevaluatedProperties` 当前会在 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` 之一
@ -86,11 +92,12 @@
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace - `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史 - active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
- 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中 - 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
- 最近验证摘要:`2026-05-06` 已完成开放对象关键字边界收口Runtime / Generator / Tooling 现统一拒绝 `patternProperties``propertyNames``unevaluatedProperties`,并保留 `additionalProperties: false` 作为唯一共享闭合对象入口;详细命令与批次背景保留在 trace 的 `2026-05-06` 记录中
- 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` 新增阶段记录中 - 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` 新增阶段记录中
- PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准active tracking 不再重复展开逐条命令历史 - PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准active tracking 不再重复展开逐条命令历史
## 下一步 ## 下一步
1. 主线继续回到 `YamlConfigSchemaValidator.cs``SchemaConfigGenerator.cs``configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf` 1. 主线继续回到 `YamlConfigSchemaValidator.cs``SchemaConfigGenerator.cs``configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf` 以及开放对象关键字扩展
2. Tooling / Docs 若要并发推进,优先补 reader-facing 示例或采用路径,不再重复扩写能力边界说明 2. Tooling / Docs 若要并发推进,优先补 reader-facing 示例或采用路径,不再重复扩写能力边界说明
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针 3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针

View File

@ -231,3 +231,52 @@
1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复 1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复
2. 若还有残留 open threads优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理 2. 若还有残留 open threads优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理
## 2026-05-06
### 阶段开放对象关键字边界收口AI-FIRST-CONFIG-RP-003
- 已在 Runtime、Source Generator 与 VS Code Tooling 三端统一收紧开放对象关键字边界
- 本轮不是扩 JSON Schema 能力,而是避免某一端静默接受会重新打开对象形状的 schema
- 当前继续接受 `additionalProperties: false` 作为显式闭合对象提醒
- `patternProperties``propertyNames``unevaluatedProperties` 当前改为三端直接失败
- reader-facing docs 也已同步更新,避免采用文档继续把这类关键字描述成“也许工具没做但运行时可能支持”的灰区
### 关键决定
- `additionalProperties: false` 仍是唯一共享支持的开放对象相关关键字形状
- 任何会重新引入动态字段集的开放对象关键字,都视为当前主线之外的设计,而不是后续工具增强项
- 本轮继续保持主线为 `C# Runtime + Source Generator + Consumer DX`,没有把工作重心切回复杂表单或宿主验证
### Stop Condition
- Batch baseline`origin/main` (`a8c6c11e`, `2026-05-05 13:14:24 +0800`)
- Primary metricbranch diff vs `origin/main` changed files阈值 `50`
- 本轮执行时的 branch diff 指标仍为 `0`,说明当前批次尚未把 `HEAD` 推进到接近阈值reviewability headroom 充足
### 验证
- 2026-05-06`bun run test``tools/gframework-config-tool`
- 结果通过133 tests
- 备注:新增 JS 回归覆盖 `patternProperties``propertyNames``unevaluatedProperties` 的显式拒绝
- 2026-05-06`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderAllOfTests"`
- 结果通过18 tests
- 备注:运行时新增开放对象关键字拒绝回归,继续沿用 `SchemaUnsupported` + `reward` 诊断路径
- 2026-05-06`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
- 结果通过57 tests
- 备注:生成器新增 `GF_ConfigSchema_016` 对称回归,覆盖 3 类开放对象关键字
- 2026-05-06`dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果通过0 warnings, 0 errors
- 2026-05-06`dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release`
- 结果通过0 warnings, 0 errors
- 2026-05-06`python3 scripts/license-header.py --check --paths ...`
- 结果:通过
- 备注:仓库脚本默认 `git ls-files` 在当前 WSL worktree 绑定下无法直接解析仓库上下文,因此本轮改为对受影响文件执行 targeted check
- 2026-05-06`git diff --check`
- 结果:通过
### 下一步
1. 继续盘点下一批不会改变生成类型形状、也不会重新打开对象形状的共享关键字
2. Tooling / Docs 如继续并发推进,优先补真实采用示例,不再重复扩写开放对象边界清单
3. 若后续 batch 再触碰 schema contract继续保持 Runtime / Generator / Tooling 三端同步失败语义与 reader-facing docs 一致

View File

@ -812,14 +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` 形态当前不属于共享支持子集,会在解析或生成阶段直接被拒绝 - `additionalProperties`:当前共享支持边界只接受 `additionalProperties: false`;它用于声明对象是闭合的,运行时、生成器和工具都会据此拒绝未声明字段。其他 `additionalProperties` 形态,以及 `patternProperties``propertyNames``unevaluatedProperties` 这类会重新打开对象形状的关键字,当前不属于共享支持子集,会在解析或生成阶段直接被拒绝
- `oneOf` / `anyOf`当前不属于共享支持子集Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字 - `oneOf` / `anyOf`当前不属于共享支持子集Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字
如果你的 schema 需要超出这些边界的复杂 shape推荐采用下面的回退顺序 如果你的 schema 需要超出这些边界的复杂 shape推荐采用下面的回退顺序
1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口 1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口
2. 再核对该 shape 是否仍符合这里列出的共享支持子集 2. 再核对该 shape 是否仍符合这里列出的共享支持子集
3. 如果它依赖 `oneOf` / `anyOf`、非 `false``additionalProperties`、会向父对象注入新字段的 `allOf` / `dependentSchemas` / `if` 分支,或者更异构的深层数组结构,就应当把它视为当前版本之外的设计,而不是工具层遗漏的“隐藏能力” 3. 如果它依赖 `oneOf` / `anyOf`、非 `false``additionalProperties``patternProperties` / `propertyNames` / `unevaluatedProperties` 这类开放对象关键字、会向父对象注入新字段的 `allOf` / `dependentSchemas` / `if` 分支,或者更异构的深层数组结构,就应当把它视为当前版本之外的设计,而不是工具层遗漏的“隐藏能力”
`allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。 `allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。

View File

@ -86,7 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer);
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema 这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。 子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及 开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false``patternProperties` / `propertyNames` / `unevaluatedProperties` 也不属于共享子集)这类已收口的对象边界,以及
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂 `oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
shape优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。 shape优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。

View File

@ -115,8 +115,8 @@ Minimal adoption checklist:
- Place each config domain under `config/<domain>/*.yaml` - Place each config domain under `config/<domain>/*.yaml`
- Place the matching schema at `schemas/<domain>.schema.json` - 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 - 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 - Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it, setting
it to `true` is outside the supported subset it to `true`, or mixing in `patternProperties`, `propertyNames`, or `unevaluatedProperties` is outside the supported subset
Use raw YAML directly when you need: Use raw YAML directly when you need:
@ -124,7 +124,8 @@ Use raw YAML directly when you need:
- supported object rules such as `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` only when they - 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 push the edit path beyond the lightweight form boundary
- `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML - `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` - schema designs outside the current shared subset, including `oneOf`, `anyOf`, non-`false` `additionalProperties`, or
other open-object keywords such as `patternProperties`, `propertyNames`, and `unevaluatedProperties`
## Documentation ## Documentation
@ -138,8 +139,9 @@ Use raw YAML directly when you need:
- Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still - Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still
fall back to raw YAML 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` / - Closed-object support is limited to `additionalProperties: false`; open-object keywords such as
`anyOf` are rejected on purpose `patternProperties`, `propertyNames`, and `unevaluatedProperties` are rejected on purpose, as are unsupported
combinators such as `oneOf` / `anyOf`
## Local Testing ## Local Testing

View File

@ -1273,23 +1273,25 @@ function parseSchemaNode(rawNode, displayPath) {
/** /**
* Reject open-object keyword forms that would drift away from the Runtime and * Reject open-object keyword forms that would drift away from the Runtime and
* Source Generator contracts. The current shared subset keeps object fields * Source Generator contracts. The current shared subset keeps object fields
* closed and only accepts an explicit `additionalProperties: false` reminder. * closed, only accepts an explicit `additionalProperties: false` reminder, and
* rejects other keywords that would reopen object shapes.
* *
* @param {Record<string, unknown>} schemaNode Raw schema object. * @param {Record<string, unknown>} schemaNode Raw schema object.
* @param {string} displayPath Logical property path. * @param {string} displayPath Logical property path.
*/ */
function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) { function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
if (!Object.prototype.hasOwnProperty.call(schemaNode, "additionalProperties")) { const unsupportedKeyword = getUnsupportedOpenObjectKeywordName(schemaNode);
if (!unsupportedKeyword) {
return; return;
} }
if (schemaNode.additionalProperties === false) { if (unsupportedKeyword === "additionalProperties" && schemaNode.additionalProperties === false) {
return; return;
} }
throw new Error( throw new Error(
`Schema property '${displayPath}' uses unsupported 'additionalProperties' metadata. ` + `Schema property '${displayPath}' uses unsupported '${unsupportedKeyword}' metadata. ` +
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed."); "The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.");
} }
/** /**
@ -1380,6 +1382,34 @@ function getUnsupportedCombinatorKeywordName(schemaNode) {
return undefined; return undefined;
} }
/**
* Return the first open-object keyword that the current shared schema subset
* intentionally rejects to keep object shapes closed and strongly typed.
*
* @param {Record<string, unknown>} schemaNode Raw schema object.
* @returns {string | undefined} Unsupported keyword name when present.
*/
function getUnsupportedOpenObjectKeywordName(schemaNode) {
if (Object.prototype.hasOwnProperty.call(schemaNode, "additionalProperties") &&
schemaNode.additionalProperties !== false) {
return "additionalProperties";
}
if (Object.prototype.hasOwnProperty.call(schemaNode, "patternProperties")) {
return "patternProperties";
}
if (Object.prototype.hasOwnProperty.call(schemaNode, "propertyNames")) {
return "propertyNames";
}
if (Object.prototype.hasOwnProperty.call(schemaNode, "unevaluatedProperties")) {
return "unevaluatedProperties";
}
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

@ -228,6 +228,63 @@ test("parseSchemaContent should reject unsupported additionalProperties forms",
/unsupported 'additionalProperties' metadata/u); /unsupported 'additionalProperties' metadata/u);
}); });
test("parseSchemaContent should reject unsupported open-object keywords", () => {
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"patternProperties": {
"^dynamic-": { "type": "integer" }
},
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
`),
/unsupported 'patternProperties' metadata/u);
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"propertyNames": {
"pattern": "^[a-z]+$"
},
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
`),
/unsupported 'propertyNames' metadata/u);
assert.throws(
() => parseSchemaContent(`
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"unevaluatedProperties": false,
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
`),
/unsupported 'unevaluatedProperties' metadata/u);
});
test("parseSchemaContent should reject unsupported explicit schema types", () => { test("parseSchemaContent should reject unsupported explicit schema types", () => {
assert.throws( assert.throws(
() => parseSchemaContent(` () => parseSchemaContent(`