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

fix(game-config): 收紧开放对象关键字边界
This commit is contained in:
gewuyou 2026-05-06 09:40:08 +08:00 committed by GitHub
commit c01abac06e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 374 additions and 36 deletions

View File

@ -884,7 +884,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
/// <summary>
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>。
/// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
@ -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;
}
/// <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>
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /

View File

@ -81,6 +81,9 @@ GameProject/
- `oneOf`
- `anyOf`
- 非 `false``additionalProperties`
- `patternProperties`
- `propertyNames`
- `unevaluatedProperties`
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束
遇到这些情况时,建议先回到 [配置系统文档](../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>
/// 验证 allOf 条目只接受 object-typed schema。
/// </summary>

View File

@ -373,7 +373,9 @@ internal static partial class YamlConfigSchemaValidator
/// <summary>
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。
/// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
@ -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;
}
/// <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>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </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>
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
/// </summary>

View File

@ -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 后续改为非阻塞并行 laneactive 入口只保留主线恢复点,把批处理细节下沉到 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 精简,只记录当前恢复点、最近验证和下一步恢复指针

View File

@ -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 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 一致
### 阶段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 噪音

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 未声明的额外同级字段继续存在
- `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` 条目不会把新字段并回父对象。

View File

@ -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 设计本体处理,再决定是否拆分结构或调整约束方式。
完整约定见:

View File

@ -115,8 +115,8 @@ Minimal adoption checklist:
- 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
- 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

View File

@ -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<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) {
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<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
* the runtime/generator diagnostics.

View File

@ -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(`