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,并继续拒绝
+ /// patternProperties、propertyNames 与
+ /// 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,并继续拒绝
+ /// patternProperties、propertyNames 与
+ /// 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(`