diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index ec9406b8..f52de49d 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -884,7 +884,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator /// /// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。 /// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的 - /// additionalProperties: false。 + /// additionalProperties: false,并继续拒绝 + /// patternPropertiespropertyNames 与 + /// unevaluatedProperties 这类会重新打开对象形状的关键字。 /// /// Schema 文件路径。 /// 逻辑字段路径。 @@ -959,12 +961,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator out Diagnostic? diagnostic) { diagnostic = null; - if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement)) - { - return true; - } - - if (additionalPropertiesElement.ValueKind == JsonValueKind.False) + if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName) { return true; } @@ -974,8 +971,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator CreateFileLocation(filePath), Path.GetFileName(filePath), displayPath, - "additionalProperties", - "The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed."); + keywordName, + "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; } @@ -991,6 +988,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator null; } + /// + /// 返回当前节点声明的首个未支持开放对象关键字。 + /// + /// 当前 schema 节点。 + /// 命中的关键字名称;未声明时返回空。 + 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; + } + /// /// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。 /// 该遍历覆盖对象属性、dependentSchemas / allOf / diff --git a/GFramework.Game.SourceGenerators/README.md b/GFramework.Game.SourceGenerators/README.md index 1be51b44..0cf7b239 100644 --- a/GFramework.Game.SourceGenerators/README.md +++ b/GFramework.Game.SourceGenerators/README.md @@ -81,6 +81,9 @@ GameProject/ - `oneOf` - `anyOf` - 非 `false` 的 `additionalProperties` +- `patternProperties` +- `propertyNames` +- `unevaluatedProperties` - 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束 遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。 diff --git a/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs b/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs index d01240eb..3f673197 100644 --- a/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs +++ b/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs @@ -448,6 +448,55 @@ public sealed class YamlConfigLoaderAllOfTests }); } + /// + /// 验证运行时会拒绝会重新打开对象形状的其他开放对象关键字。 + /// + [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(() => 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)); + }); + } + /// /// 验证 allOf 条目只接受 object-typed schema。 /// diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs index f520907c..45381133 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs @@ -373,7 +373,9 @@ internal static partial class YamlConfigSchemaValidator /// /// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。 /// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的 - /// additionalProperties: false,继续拒绝会引入动态字段形状的其它形式。 + /// additionalProperties: false,并继续拒绝 + /// patternPropertiespropertyNames 与 + /// unevaluatedProperties 这类会重新打开对象形状的关键字。 /// /// 所属配置表名称。 /// Schema 文件路径。 @@ -385,12 +387,7 @@ internal static partial class YamlConfigSchemaValidator string propertyPath, JsonElement element) { - if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement)) - { - return; - } - - if (additionalPropertiesElement.ValueKind == JsonValueKind.False) + if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName) { return; } @@ -398,8 +395,8 @@ internal static partial class YamlConfigSchemaValidator 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.", + $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " + + "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, displayPath: GetDiagnosticPath(propertyPath)); } @@ -416,6 +413,25 @@ internal static partial class YamlConfigSchemaValidator null; } + /// + /// 返回当前节点声明的首个未支持开放对象关键字。 + /// + /// 当前 schema 节点。 + /// 命中的关键字名称;未声明时返回空。 + 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; + } + /// /// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。 /// diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index 9d5c213c..507bb1b0 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -1965,6 +1965,58 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证生成器会拒绝会重新打开对象形状的其他开放对象关键字。 + /// + [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")); + }); + } + /// /// 验证 then 子 schema 内的非法 format 也会在生成阶段直接给出诊断。 /// diff --git a/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md b/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md index 13939f27..4c713c76 100644 --- a/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md +++ b/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md @@ -12,6 +12,7 @@ - 当前焦点: - 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字 - 已明确将 `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 - 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema - Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件 @@ -20,6 +21,8 @@ - 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移 - 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集 +- 开放对象形状风险:如果某一端静默接受 `patternProperties`、`propertyNames`、`unevaluatedProperties` 等关键字,会重新打开对象形状并造成契约漂移 + - 缓解措施:当前三端已统一把开放对象边界收紧为只接受 `additionalProperties: false`,其余开放对象关键字直接报错 - 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足 - 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证 - PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments @@ -40,6 +43,9 @@ - `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else` - 已明确拒绝会改变生成类型形状的组合关键字: - `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略 +- 已明确拒绝会重新打开对象形状的开放对象关键字: + - 当前只接受 `additionalProperties: false` + - `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略 - `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地: - 只允许 object 节点上的 object-typed inline schema - `if` 必填,且必须至少伴随 `then` 或 `else` 之一 @@ -86,11 +92,13 @@ - `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace - active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史 - 最近验证摘要:`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` 记录中 +- 最近验证摘要:`2026-05-06` 已按 PR `#325` latest review follow-up 移除三端开放对象校验中的不可达 `additionalProperties: false` 放行分支,补齐 Tooling 正向回归,并同步拆分 reader-facing docs 对开放对象边界的表述;细节与验证命令保留在 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 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 示例或采用路径,不再重复扩写能力边界说明 3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针 diff --git a/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md b/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md index 589cb4ff..45956eda 100644 --- a/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md +++ b/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md @@ -231,3 +231,96 @@ 1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复 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 metric:branch 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 一致 + +### 阶段:PR #325 latest review follow-up 收口(AI-FIRST-CONFIG-RP-003) + +- 已使用 `gframework-pr-review` 抓取并复核 PR `#325` 的 latest review body、未解决 latest-head 线程、MegaLinter 摘要与测试报告 +- 本轮按“仅修复本地仍成立项”收口 5 条 review 信号: + - Runtime / Generator / Tooling 三端均移除开放对象关键字校验中的不可达 `additionalProperties: false` 放行分支 + - Tooling 测试补齐 `additionalProperties: false` 的正向回归,避免共享允许边界后续回退 + - `docs/zh-CN/game/index.md` 将开放对象边界说明拆成并列语句,避免把 `patternProperties` / `propertyNames` / `unevaluatedProperties` 误读成 `additionalProperties` 的变体 +- 本轮没有跟进 stale 信号: + - PR 当前 failed checks 为 `0` + - latest test report 为 `2280 passed / 0 failed` + - MegaLinter 仅保留 `dotnet-format` 摘要噪音,未提供需要额外修复的新代码格式差异 + +### 验证 + +- 2026-05-06:`python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` + - 结果:通过 + - 备注:确认 PR `#325` 仍有 3 条 CodeRabbit nitpick 与 2 条 Greptile open threads,需要本地核验 +- 2026-05-06:`node --test ./test/*.test.js`(`tools/gframework-config-tool`) + - 结果:通过(134 tests) + - 备注:新增 `additionalProperties: false` 正向回归后,工具端继续显式接受唯一共享闭合对象入口 +- 2026-05-06:`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderAllOfTests"` + - 结果:通过(18 tests) + - 备注:运行时开放对象关键字回归保持通过,未引入额外诊断路径漂移 +- 2026-05-06:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"` + - 结果:通过(57 tests) + - 备注:生成器开放对象关键字诊断回归保持通过,移除不可达分支未影响既有诊断契约 +- 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` + - 结果:环境受限 + - 备注:仓库脚本默认通过 `git ls-files` 枚举文件,在当前 WSL worktree 绑定下返回 `128`;已改为对受影响文件执行 targeted check 并通过 +- 2026-05-06:`python3 scripts/license-header.py --check --paths GFramework.Game/Config/YamlConfigSchemaValidator.cs GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs tools/gframework-config-tool/src/configValidation.js tools/gframework-config-tool/test/configValidation.test.js` + - 结果:通过 +- 2026-05-06:`git diff --check` + - 结果:通过 + +### 下一步 + +1. 执行本轮受影响 Tooling / Runtime / Generator 定向验证,并确认没有新增 warning 或格式漂移 +2. 若验证通过,重新抓取 PR `#325` review 状态,区分哪些 open threads 会随推送自动折叠 +3. 继续把 PR review follow-up 约束在“latest unresolved thread + 本地仍成立问题”,不回头追旧 summary 噪音 diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index 0d17cd11..813d385d 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -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 未声明的额外同级字段继续存在 - `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 语义,允许对象保留未在条件块中声明的额外同级字段 -- `additionalProperties`:当前共享支持边界只接受 `additionalProperties: false`;它用于声明对象是闭合的,运行时、生成器和工具都会据此拒绝未声明字段。其他 `additionalProperties` 形态当前不属于共享支持子集,会在解析或生成阶段直接被拒绝 +- `additionalProperties`:当前共享支持边界只接受 `additionalProperties: false`;它用于声明对象是闭合的,运行时、生成器和工具都会据此拒绝未声明字段。其他 `additionalProperties` 形态,以及 `patternProperties`、`propertyNames`、`unevaluatedProperties` 这类会重新打开对象形状的关键字,当前都不属于共享支持子集,会在解析或生成阶段直接被拒绝 - `oneOf` / `anyOf`:当前不属于共享支持子集;Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字 如果你的 schema 需要超出这些边界的复杂 shape,推荐采用下面的回退顺序: 1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口 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` 条目不会把新字段并回父对象。 diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md index 69eb146d..08036b88 100644 --- a/docs/zh-CN/game/index.md +++ b/docs/zh-CN/game/index.md @@ -86,8 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer); 这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema 子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。 -开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及 -`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂 +开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false` 这类已收口的对象边界:它必须显式设置为 `false`,省略或 `true` 都视为非 `false`。`patternProperties` / `propertyNames` / `unevaluatedProperties` 当前也不属于共享子集。`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂 shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。 完整约定见: diff --git a/tools/gframework-config-tool/README.md b/tools/gframework-config-tool/README.md index 1f0473a2..35af86d4 100644 --- a/tools/gframework-config-tool/README.md +++ b/tools/gframework-config-tool/README.md @@ -115,8 +115,8 @@ Minimal adoption checklist: - Place each config domain under `config//*.yaml` - Place the matching schema at `schemas/.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 +- Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it, setting + it to `true`, or mixing in `patternProperties`, `propertyNames`, or `unevaluatedProperties` is outside the supported subset 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 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` +- 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 @@ -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 fall back to raw YAML - 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 +- Closed-object support is limited to `additionalProperties: false`; open-object keywords such as + `patternProperties`, `propertyNames`, and `unevaluatedProperties` are rejected on purpose, as are unsupported + combinators such as `oneOf` / `anyOf` ## Local Testing diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 72d87004..60e31f8e 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -1273,23 +1273,21 @@ function parseSchemaNode(rawNode, 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. + * closed, only accepts an explicit `additionalProperties: false` reminder, and + * rejects other keywords that would reopen object shapes. * * @param {Record} 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) { + const unsupportedKeyword = getUnsupportedOpenObjectKeywordName(schemaNode); + if (!unsupportedKeyword) { 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."); + `Schema property '${displayPath}' uses unsupported '${unsupportedKeyword}' metadata. ` + + "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 +1378,34 @@ function getUnsupportedCombinatorKeywordName(schemaNode) { 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} 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 * the runtime/generator diagnostics. diff --git a/tools/gframework-config-tool/test/configValidation.test.js b/tools/gframework-config-tool/test/configValidation.test.js index 2c913825..dd9bd723 100644 --- a/tools/gframework-config-tool/test/configValidation.test.js +++ b/tools/gframework-config-tool/test/configValidation.test.js @@ -228,6 +228,80 @@ test("parseSchemaContent should reject unsupported additionalProperties forms", /unsupported 'additionalProperties' metadata/u); }); +test("parseSchemaContent should allow explicit additionalProperties false", () => { + assert.doesNotThrow(() => parseSchemaContent(` + { + "type": "object", + "properties": { + "reward": { + "type": "object", + "additionalProperties": false, + "properties": { + "itemCount": { "type": "integer" } + } + } + } + } + `)); +}); + +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", () => { assert.throws( () => parseSchemaContent(`