diff --git a/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md index 53562b2d..6761cb78 100644 --- a/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -21,3 +21,4 @@ GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_015 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_016 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics + GF_ConfigSchema_017 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index f52de49d..bed65d44 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -237,6 +237,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return TryValidateStringFormatMetadataRecursively(filePath, "", root, out diagnostic) && TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "", root, out diagnostic) && TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "", root, out diagnostic) && + TryValidateUnsupportedArrayShapeKeywordsRecursively(filePath, "", root, out diagnostic) && TryValidateDependentRequiredMetadataRecursively(filePath, "", root, out diagnostic) && TryValidateDependentSchemasMetadataRecursively(filePath, "", root, out diagnostic) && TryValidateAllOfMetadataRecursively(filePath, "", root, out diagnostic) && @@ -916,6 +917,40 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator out diagnostic); } + /// + /// 递归拒绝当前共享子集尚未支持的数组形状关键字。 + /// 当前配置系统只接受单个 object-typed items schema, + /// 并继续拒绝 tuple / open-array 关键字,避免生成器对数组元素形状 + /// 比运行时与工具链更宽松。 + /// + /// Schema 文件路径。 + /// 逻辑字段路径。 + /// 当前 schema 节点。 + /// 失败时返回的诊断。 + /// 当前节点树是否未声明不支持的数组形状关键字。 + private static bool TryValidateUnsupportedArrayShapeKeywordsRecursively( + string filePath, + string displayPath, + JsonElement element, + out Diagnostic? diagnostic) + { + return TryTraverseSchemaRecursively( + filePath, + displayPath, + element, + static (currentFilePath, currentDisplayPath, currentElement, _) => + { + return TryValidateUnsupportedArrayShapeKeywords( + currentFilePath, + currentDisplayPath, + currentElement, + out var currentDiagnostic) + ? (true, (Diagnostic?)null) + : (false, currentDiagnostic); + }, + out diagnostic); + } + /// /// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。 /// @@ -976,6 +1011,36 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return false; } + /// + /// 验证当前节点是否声明了当前共享子集尚未支持的数组形状关键字。 + /// + /// Schema 文件路径。 + /// 逻辑字段路径。 + /// 当前 schema 节点。 + /// 失败时返回的诊断。 + /// 未声明不支持关键字时返回 + private static bool TryValidateUnsupportedArrayShapeKeywords( + string filePath, + string displayPath, + JsonElement element, + out Diagnostic? diagnostic) + { + diagnostic = null; + if (TryGetUnsupportedArrayShapeKeywordName(element) is not { } keywordName) + { + return true; + } + + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.UnsupportedArrayShapeKeyword, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + displayPath, + keywordName, + "The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling."); + return false; + } + /// /// 返回当前节点声明的首个未支持组合关键字。 /// @@ -1007,6 +1072,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator null; } + /// + /// 返回当前节点声明的首个未支持数组形状关键字。 + /// + /// 当前 schema 节点。 + /// 命中的关键字名称;未声明时返回空。 + private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element) + { + return element.TryGetProperty("prefixItems", out _) ? "prefixItems" : + element.TryGetProperty("additionalItems", out _) ? "additionalItems" : + element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" : + null; + } + /// /// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。 /// 该遍历覆盖对象属性、dependentSchemas / allOf / diff --git a/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs b/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs index 077ced12..bb0cd1bc 100644 --- a/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs +++ b/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs @@ -187,4 +187,15 @@ public static class ConfigSchemaDiagnostics SourceGeneratorsConfigCategory, DiagnosticSeverity.Error, true); + + /// + /// schema 节点声明了当前共享子集尚未支持的数组形状关键字。 + /// + public static readonly DiagnosticDescriptor UnsupportedArrayShapeKeyword = new( + "GF_ConfigSchema_017", + "Config schema uses an unsupported array-shape keyword", + "Property '{1}' in schema file '{0}' uses unsupported array-shape keyword '{2}': {3}", + SourceGeneratorsConfigCategory, + DiagnosticSeverity.Error, + true); } diff --git a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs index d60ac8f6..751c58cd 100644 --- a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs +++ b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs @@ -1636,6 +1636,68 @@ public class YamlConfigLoaderTests }); } + /// + /// 验证数组字段声明 tuple / open-array 关键字时,会在 schema 解析阶段被显式拒绝。 + /// + /// 待验证的数组形状关键字名称。 + /// 用于拼接测试 schema 的关键字值 JSON 片段。 + [TestCase("prefixItems", """ + [ + { "type": "integer" } + ] + """)] + [TestCase("additionalItems", "false")] + [TestCase("unevaluatedItems", "false")] + public void LoadAsync_Should_Throw_When_Array_Schema_Declares_Unsupported_ArrayShape_Keyword( + string keywordName, + string keywordValueJson) + { + CreateConfigFile( + "monster/slime.yaml", + """ + id: 1 + name: Slime + dropRates: + - 5 + """); + CreateSchemaFile( + "schemas/monster.schema.json", + $$""" + { + "type": "object", + "required": ["id", "name", "dropRates"], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "dropRates": { + "type": "array", + "{{keywordName}}": {{keywordValueJson}}, + "items": { + "type": "integer" + } + } + } + } + """); + + var loader = new YamlConfigLoader(_rootPath) + .RegisterTable("monster", "monster", "schemas/monster.schema.json", + static config => config.Id); + var registry = new ConfigRegistry(); + + 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("dropRates")); + Assert.That(exception.Message, Does.Contain($"unsupported '{keywordName}' metadata")); + Assert.That(exception.Message, Does.Contain("only accepts one object-valued 'items' schema")); + Assert.That(registry.Count, Is.EqualTo(0)); + }); + } + /// /// 验证对象数组的 contains 试匹配会按声明属性子集工作,而不会因额外字段误判为不匹配。 /// diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs index 45381133..ebfb0d10 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs @@ -326,6 +326,7 @@ internal static partial class YamlConfigSchemaValidator { ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element); ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element); + ValidateUnsupportedArrayShapeKeywords(tableName, schemaPath, propertyPath, element); var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element); var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element); ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName); @@ -401,6 +402,36 @@ internal static partial class YamlConfigSchemaValidator displayPath: GetDiagnosticPath(propertyPath)); } + /// + /// 显式拒绝当前共享子集中尚未支持的数组形状关键字。 + /// 当前配置系统只接受单个 object-valued items schema, + /// 并继续拒绝 tuple / open-array 关键字,避免 Runtime / Generator / Tooling + /// 对数组元素形状产生静默漂移。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 当前节点的逻辑属性路径。 + /// 当前 schema 节点。 + private static void ValidateUnsupportedArrayShapeKeywords( + string tableName, + string schemaPath, + string propertyPath, + JsonElement element) + { + if (TryGetUnsupportedArrayShapeKeywordName(element) is not { } keywordName) + { + return; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " + + "The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + /// /// 返回当前节点声明的首个未支持组合关键字。 /// @@ -432,6 +463,19 @@ internal static partial class YamlConfigSchemaValidator null; } + /// + /// 返回当前节点声明的首个未支持数组形状关键字。 + /// + /// 当前 schema 节点。 + /// 命中的关键字名称;未声明时返回空。 + private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element) + { + return element.TryGetProperty("prefixItems", out _) ? "prefixItems" : + element.TryGetProperty("additionalItems", out _) ? "additionalItems" : + element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" : + null; + } + /// /// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。 /// diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index 507bb1b0..83c137e1 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -2017,6 +2017,56 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证生成器会显式拒绝当前共享子集尚未支持的数组形状关键字。 + /// + /// 待验证的数组形状关键字名称。 + /// 用于拼接测试 schema 的关键字值 JSON 片段。 + [TestCase("prefixItems", """ + [ + { "type": "integer" } + ] + """)] + [TestCase("additionalItems", "false")] + [TestCase("unevaluatedItems", "false")] + public void Run_Should_Report_Diagnostic_When_Array_Schema_Declares_Unsupported_ArrayShape_Keyword( + string keywordName, + string keywordValueJson) + { + const string source = DummySource; + var schema = $$""" + { + "type": "object", + "required": ["id", "dropRates"], + "properties": { + "id": { "type": "integer" }, + "dropRates": { + "type": "array", + "{{keywordName}}": {{keywordValueJson}}, + "items": { + "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_017")); + Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error)); + Assert.That(diagnostic.GetMessage(), Does.Contain("dropRates")); + Assert.That(diagnostic.GetMessage(), Does.Contain(keywordName)); + Assert.That(diagnostic.GetMessage(), Does.Contain("only accepts one object-valued 'items' schema")); + }); + } + /// /// 验证 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 4c713c76..f862a6d6 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 @@ -13,6 +13,7 @@ - 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字 - 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移 - 已把开放对象关键字边界收紧为只接受 `additionalProperties: false`,并在 Runtime / Generator / Tooling 三端显式拒绝 `patternProperties`、`propertyNames`、`unevaluatedProperties` + - 已把数组形状关键字边界收紧为只接受单个 object-valued `items` schema,并在 Runtime / Generator / Tooling 三端显式拒绝 `prefixItems`、`additionalItems`、`unevaluatedItems` - 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests - 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema - Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件 @@ -23,6 +24,8 @@ - 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集 - 开放对象形状风险:如果某一端静默接受 `patternProperties`、`propertyNames`、`unevaluatedProperties` 等关键字,会重新打开对象形状并造成契约漂移 - 缓解措施:当前三端已统一把开放对象边界收紧为只接受 `additionalProperties: false`,其余开放对象关键字直接报错 +- 数组形状漂移风险:如果某一端静默接受 `prefixItems`、`additionalItems`、`unevaluatedItems` 等关键字,会让 tuple / open-array 设计在三端表现不一致 + - 缓解措施:当前三端已统一把数组形状边界收紧为只接受单个 object-valued `items` schema,其余数组形状关键字直接报错 - 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足 - 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证 - PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments @@ -46,6 +49,9 @@ - 已明确拒绝会重新打开对象形状的开放对象关键字: - 当前只接受 `additionalProperties: false` - `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略 +- 已明确拒绝会改变数组元素形状的数组关键字: + - 当前只接受单个 object-valued `items` schema + - `prefixItems`、`additionalItems`、`unevaluatedItems` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略 - `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地: - 只允许 object 节点上的 object-typed inline schema - `if` 必填,且必须至少伴随 `then` 或 `else` 之一 @@ -94,6 +100,9 @@ - 最近验证摘要:`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` 追加记录中 +- 最近验证摘要:`2026-05-06` 已核对 `extension.js` 的对象数组编辑能力与 reader-facing 文档,确认表单当前支持对象数组项内部继续嵌套的对象数组;`tools/gframework-config-tool/README.md` 与 `docs/zh-CN/game/config-tool.md` 已同步收紧回退条件,避免把“仍在共享子集内的嵌套对象数组”误写成默认只能回退 raw YAML;细节与验证命令保留在 trace 的 `2026-05-06` 追加记录中 +- 最近验证摘要:`2026-05-08` 已把数组形状边界收紧为只接受单个 object-valued `items` schema;Runtime / Generator / Tooling 现统一拒绝 `prefixItems`、`additionalItems`、`unevaluatedItems`,并补齐三端回归测试与 reader-facing 文档说明;细节与验证命令保留在 trace 的 `2026-05-08` 记录中 +- 最近验证摘要:`2026-05-09` 已按 PR `#343` latest unresolved review threads 补齐两个 public 参数化测试的 XML `` 注释,并为 `2026-05-06` Tooling 文档批次标题追加上下文后缀以消除 `MD024`;细节与定向验证命令保留在 trace 的 `2026-05-09` 记录中 - 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 不再重复展开逐条命令历史 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 45956eda..82910473 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 @@ -145,7 +145,7 @@ - 指向真正承载并行批次细节的 backlog 文件 - 本轮不新增代码范围、测试范围或文档范围,只整理 public `ai-plan/**` 的恢复入口表达,避免把治理噪音带回 reader-facing docs -### 关键决定 +### 关键决定(Tooling 文档与实际编辑边界对齐) - `C# Runtime + Source Generator + Consumer DX` 仍是默认恢复主线 - Tooling / Docs 可以并发推进,但后续 batch 应直接以 `ai-first-config-system-csharp-experience-next.md` 为入口,而不是继续扩写 active tracking / trace @@ -242,19 +242,19 @@ - `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前改为三端直接失败 - reader-facing docs 也已同步更新,避免采用文档继续把这类关键字描述成“也许工具没做但运行时可能支持”的灰区 -### 关键决定 +### 关键决定(Tooling 文档与实际编辑边界对齐) - `additionalProperties: false` 仍是唯一共享支持的开放对象相关关键字形状 - 任何会重新引入动态字段集的开放对象关键字,都视为当前主线之外的设计,而不是后续工具增强项 - 本轮继续保持主线为 `C# Runtime + Source Generator + Consumer DX`,没有把工作重心切回复杂表单或宿主验证 -### Stop Condition +### Stop Condition(Tooling 文档与实际编辑边界对齐) - 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 充足 -### 验证 +### 验证(Tooling 文档与实际编辑边界对齐) - 2026-05-06:`bun run test`(`tools/gframework-config-tool`) - 结果:通过(133 tests) @@ -275,7 +275,7 @@ - 2026-05-06:`git diff --check` - 结果:通过 -### 下一步 +### 下一步(Tooling 文档与实际编辑边界对齐) 1. 继续盘点下一批不会改变生成类型形状、也不会重新打开对象形状的共享关键字 2. Tooling / Docs 如继续并发推进,优先补真实采用示例,不再重复扩写开放对象边界清单 @@ -324,3 +324,103 @@ 1. 执行本轮受影响 Tooling / Runtime / Generator 定向验证,并确认没有新增 warning 或格式漂移 2. 若验证通过,重新抓取 PR `#325` review 状态,区分哪些 open threads 会随推送自动折叠 3. 继续把 PR review follow-up 约束在“latest unresolved thread + 本地仍成立问题”,不回头追旧 summary 噪音 + +### 阶段:Tooling 文档与实际编辑边界对齐(AI-FIRST-CONFIG-RP-003) + +- 已重新核对 `tools/gframework-config-tool/src/extension.js` 的对象数组表单能力,并确认当前实现不只支持对象数组本身 +- 当前表单还支持“对象数组项内部继续嵌套的对象数组”,前提是内层条目仍保持共享子集允许的对象 / 标量字段 / 标量数组 / 嵌套对象形状 +- 本轮不扩 Runtime / Generator / Tooling 的 schema 契约,只修正 reader-facing docs 漂移: + - `tools/gframework-config-tool/README.md` 不再把“更深层嵌套编辑”笼统描述成默认回退 raw YAML + - `docs/zh-CN/game/config-tool.md` 改为明确:只有当对象数组继续嵌套后的结构超出当前共享子集,才需要回到 raw YAML + +### 关键决定(Tooling 文档与实际编辑边界对齐) + +- 这轮批次继续遵守“先核对共享契约,再改文档”的 lane 规则,没有为追求批量推进而硬扩一个收益不明确的新关键字 +- Tooling 的 reader-facing 说明要以 `extension.js` 当前真实能力为准,避免把已经支持的对象数组路径继续描述成工具缺口 +- raw YAML 回退条件保留,但需要收敛为“超出共享子集或当前编辑器边界”而不是“只要更深层对象数组就默认回退” + +### Stop Condition(Tooling 文档与实际编辑边界对齐) + +- Batch baseline:`origin/main` (`c01abac0`, `2026-05-06 09:40:08 +0800`) +- Primary metric:branch diff vs `origin/main` changed files,阈值 `30` +- 本轮开始前 branch diff 指标为 `0` files / `0` changed lines;本批次只触碰 reader-facing docs 与 active `ai-plan`,预计仍远低于阈值 + +### 验证(Tooling 文档与实际编辑边界对齐) + +- 2026-05-06:`rg -n "nested object arrays|回退到 raw YAML|更深层对象数组"`(文档 + `extension.js`) + - 结果:通过 + - 备注:确认 `README` / 中文工具文档存在旧边界表述,而 `extension.js` 已支持对象数组项内部继续嵌套的对象数组编辑 +- 2026-05-06:`git diff --check -- tools/gframework-config-tool/README.md docs/zh-CN/game/config-tool.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md` + - 结果:通过 +- 2026-05-06:`python3 scripts/license-header.py --check --paths tools/gframework-config-tool/README.md docs/zh-CN/game/config-tool.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md` + - 结果:通过 +- 2026-05-06:`dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:通过(0 warnings, 0 errors) + +### 下一步(Tooling 文档与实际编辑边界对齐) + +1. 继续优先找“实现已存在但 reader-facing 表述漂移”的低风险 lane,避免在批处理模式下引入收益不明的新 schema contract +2. 若下一轮回到主线代码批次,再继续盘点不会改变生成类型形状的共享关键字,而不是重复刷新同一组 Tooling 边界说明 + +## 2026-05-08 + +### 阶段:数组形状关键字边界收口(AI-FIRST-CONFIG-RP-003) + +- 已在 Runtime、Source Generator 与 VS Code Tooling 三端统一收紧数组形状关键字边界 +- 本轮不是扩 JSON Schema 能力,而是避免某一端静默接受 tuple / open-array 设计: + - 当前共享子集只接受单个 object-valued `items` schema + - `prefixItems`、`additionalItems`、`unevaluatedItems` 现在会在三端直接失败,而不是静默忽略 +- reader-facing docs 也已同步补齐数组形状边界,避免把“标准 JSON Schema 的 tuple/open-array 语义”误读成当前配置系统的隐藏支持范围 + +### 验证 + +- 2026-05-08:`bun run test`(`tools/gframework-config-tool`) + - 目标:验证工具端会拒绝 `prefixItems`、`additionalItems`、`unevaluatedItems` +- 2026-05-08:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"` + - 目标:验证生成器新增 `GF_ConfigSchema_017` +- 2026-05-08:`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests"` + - 目标:验证运行时会拒绝不在共享子集内的数组形状关键字 +- 2026-05-08:`dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 目标:验证运行时模块 Release 构建 +- 2026-05-08:`dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release` + - 目标:验证生成器模块 Release 构建 + +### 下一步 + +1. 若本轮验证通过,继续回到“不会改变生成类型形状”的下一批共享关键字盘点 +2. 继续优先寻找“静默接受但主线不支持”的边界收口项,而不是先扩更复杂的组合语义 + +## 2026-05-09 + +### 阶段:PR #343 review follow-up(AI-FIRST-CONFIG-RP-003) + +- 已用 `gframework-pr-review` 重新抓取当前分支 PR `#343` 的 latest unresolved review threads +- 本轮只处理本地仍成立的 3 条 open threads: + - `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs` 的 public 参数化测试补齐 XML `` 注释 + - `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs` 的 public 参数化测试补齐 XML `` 注释 + - `ai-first-config-system-trace.md` 为 `2026-05-06` Tooling 文档批次的重复三级标题加上上下文后缀,消除 `MD024` +- 其余 PR 信号已核对: + - 当前没有 failed checks + - MegaLinter 仅报告 1 条 `dotnet-format` 相关问题;本轮先按 latest open review threads 收口本地仍成立项 + - GitHub Test Reporter 显示 `2336` tests passed、`0` failed + +### 验证(PR #343 review follow-up) + +- 2026-05-09:`python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` + - 结果:通过 + - 备注:确认 PR `#343` 当前仍有 3 条 latest unresolved CodeRabbit threads,且都可在本地直接复现 +- 2026-05-09:`git diff --check -- GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md` + - 结果:通过 +- 2026-05-09:`python3 scripts/license-header.py --check --paths GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs` + - 结果:通过 +- 2026-05-09:`dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` + - 结果:通过(0 warnings, 0 errors) + - 备注:沙箱内并行 restore 命中了 `.nuget.g.props` 已存在的环境型冲突;已按仓库规则在沙箱外重跑原命令,并以外部结果作为准确信号 +- 2026-05-09:`dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过(0 warnings, 0 errors) + - 备注:已在沙箱外串行重跑原命令,确认本轮 PR review 修复未引入构建问题 + +### 下一步(PR #343 review follow-up) + +1. 若本轮定向校验通过,重新抓取 PR `#343` review 状态,确认这 3 条 open threads 是否已具备自动折叠条件 +2. 若 PR 仍残留 review 信号,继续只处理 latest unresolved thread 中本地仍成立的问题,不回头追旧 summary 噪音 diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index 813d385d..b4824203 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -17,7 +17,7 @@ description: 说明 GFramework.Game 配置系统的定位、目录约定、生 - JSON Schema 作为结构描述 - 一对象一文件的目录组织 - 运行时只读查询 -- Runtime / Generator / Tooling 共享支持 `enum`、`const`、`not`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`,以及闭合对象边界 `additionalProperties: false` +- Runtime / Generator / Tooling 共享支持 `enum`、`const`、`not`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`,以及闭合对象边界 `additionalProperties: false`;数组形状当前只接受单个 object-valued `items` schema - Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录 - VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口 @@ -813,13 +813,14 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re - `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` 形态,以及 `patternProperties`、`propertyNames`、`unevaluatedProperties` 这类会重新打开对象形状的关键字,当前都不属于共享支持子集,会在解析或生成阶段直接被拒绝 +- 数组形状关键字:当前共享支持边界只接受单个 object-valued `items` schema;`prefixItems`、`additionalItems`、`unevaluatedItems` 这类 tuple / open-array 关键字当前都不属于共享支持子集,会在解析或生成阶段直接被拒绝,避免数组元素形状在三端静默漂移 - `oneOf` / `anyOf`:当前不属于共享支持子集;Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字 如果你的 schema 需要超出这些边界的复杂 shape,推荐采用下面的回退顺序: 1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口 2. 再核对该 shape 是否仍符合这里列出的共享支持子集 -3. 如果它依赖 `oneOf` / `anyOf`、非 `false` 的 `additionalProperties`、`patternProperties` / `propertyNames` / `unevaluatedProperties` 这类开放对象关键字、会向父对象注入新字段的 `allOf` / `dependentSchemas` / `if` 分支,或者更异构的深层数组结构,就应当把它视为当前版本之外的设计,而不是工具层遗漏的“隐藏能力” +3. 如果它依赖 `oneOf` / `anyOf`、非 `false` 的 `additionalProperties`、`patternProperties` / `propertyNames` / `unevaluatedProperties` 这类开放对象关键字、`prefixItems` / `additionalItems` / `unevaluatedItems` 这类 tuple / open-array 关键字、会向父对象注入新字段的 `allOf` / `dependentSchemas` / `if` 分支,或者更异构的深层数组结构,就应当把它视为当前版本之外的设计,而不是工具层遗漏的“隐藏能力” `allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。 diff --git a/docs/zh-CN/game/config-tool.md b/docs/zh-CN/game/config-tool.md index 9cdb7c34..933fc7f8 100644 --- a/docs/zh-CN/game/config-tool.md +++ b/docs/zh-CN/game/config-tool.md @@ -22,7 +22,7 @@ description: 说明 GFramework AI-First 配置工作流对应的 VS Code 工具 - 项目不使用 `GFramework.Game` 的配置工作流 - 需要完整 JSON Schema 编辑器,而不是当前仓库落地的稳定子集 -- 需要在编辑器里处理更深层对象数组嵌套,且不接受回退到 raw YAML +- 需要完整放宽到当前共享支持子集之外的更异构数组结构,且不接受回退到 raw YAML ## 工作区约定 @@ -83,7 +83,7 @@ Explorer + 表单预览。 - 对空配置文件提供基于 schema 的示例 YAML 初始化入口 - 对同一配置域内的多份 YAML 文件执行批量字段更新 - 在表单入口中显示 `title / description / default / const / enum / x-gframework-ref-table(UI 中显示为 ref-table) / multipleOf / pattern / format / uniqueItems / contains / minContains / maxContains / minProperties / maxProperties / dependentRequired / dependentSchemas / allOf / if / then / else` 元数据;批量编辑入口当前只暴露顶层可批量改写字段所需的基础信息 -- 对 `additionalProperties: false` 提供闭合对象边界校验,并在遇到 `oneOf` / `anyOf` 或其他当前未收口的组合形状时明确提示该 schema 不属于当前工具支持子集 +- 对 `additionalProperties: false` 提供闭合对象边界校验,并在遇到 `oneOf` / `anyOf`、`prefixItems` / `additionalItems` / `unevaluatedItems` 或其他当前未收口的组合 / 数组形状时明确提示该 schema 不属于当前工具支持子集 当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。 @@ -195,6 +195,7 @@ rewardTableId: starter-reward - `additionalProperties` 是否显式设置为 `false`;省略或 `true` 不属于当前共享支持子集 - schema 是否依赖 `oneOf` / `anyOf`;这些组合关键字会被 Runtime / Generator / Tooling 直接拒绝 +- schema 是否依赖 `prefixItems` / `additionalItems` / `unevaluatedItems`;当前数组子集只接受单个 object-valued `items` schema - 对象数组里是否混入标量项,或是否存在更深、更异构的数组结构 - Runtime / Source Generator 是否已经接受这份 schema,而不是只有编辑器里“暂时看起来能写” @@ -230,15 +231,16 @@ rewardTableId: starter-reward - `contains` / `minContains` / `maxContains` - `additionalProperties: false` -如果你进入更深层对象数组嵌套,当前更稳妥的做法通常是: +如果你进入对象数组里的继续嵌套对象数组,当前表单通常仍可继续处理;更稳妥的顺序是: 1. 用 Explorer 找到目标文件 -2. 先看表单预览确认字段结构 -3. 再回到 raw YAML 完成最终编辑 +2. 先看表单预览确认当前层级仍保持对象 / 标量字段 / 标量数组 / 嵌套对象的共享子集形状 +3. 只有在结构开始变得更异构,或已经超出当前共享支持子集时,再回到 raw YAML 完成最终编辑 以下 shape 目前也建议直接回退到 raw YAML,并同时检查 schema 是否仍在当前共享支持子集内: - 需要表达 `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字 +- 需要表达 `prefixItems` / `additionalItems` / `unevaluatedItems` 这类 tuple / open-array 关键字 - 需要 `additionalProperties` 的其他形态,而不是当前明确支持的 `additionalProperties: false` - 需要在 `allOf`、`dependentSchemas`、`if` / `then` / `else` 中引入父对象未声明的新字段 - 需要比当前对象数组编辑器更深、更异构的数组结构 @@ -265,9 +267,9 @@ rewardTableId: starter-reward - 工作区默认只取第一个 workspace folder - 校验聚焦仓库当前支持的 schema 子集 -- 表单预览支持对象数组,但更深的嵌套对象数组仍可能需要回退到 raw YAML +- 表单预览支持对象数组,以及对象数组项内部继续嵌套的对象数组;只有当内层结构超出共享子集时才需要回退到 raw YAML - 批量编辑当前聚焦顶层标量和顶层标量数组字段 -- 共享约束里只支持闭合对象边界 `additionalProperties: false`;`oneOf` / `anyOf` 等改变生成形状的组合关键字会被明确拒绝 +- 共享约束里只支持闭合对象边界 `additionalProperties: false`,数组形状里只支持单个 object-valued `items` schema;`oneOf` / `anyOf` 与 `prefixItems` / `additionalItems` / `unevaluatedItems` 这类会改变生成形状的关键字会被明确拒绝 因此,最稳妥的理解方式是: diff --git a/tools/gframework-config-tool/README.md b/tools/gframework-config-tool/README.md index 35af86d4..7a9196ec 100644 --- a/tools/gframework-config-tool/README.md +++ b/tools/gframework-config-tool/README.md @@ -80,6 +80,8 @@ The extension currently validates the repository's current schema subset: object-focused `if` / `then` / `else` - closed-object validation through `additionalProperties: false` - explicit rejection for unsupported combinators such as `oneOf` and `anyOf`, instead of silently ignoring them +- explicit rejection for unsupported array-shape keywords such as `prefixItems`, `additionalItems`, and + `unevaluatedItems`, so tuple or open-array item shapes do not drift away from Runtime / Generator ## Contract Boundary @@ -106,8 +108,8 @@ This extension is an editor-side helper. It does not define the runtime contract project-specific paths relative to the first workspace folder. 3. Open the `GFramework Config` explorer view and select a config file or domain. 4. Run validation first to confirm the current YAML files still match the supported schema subset. -5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits - when needed. +5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML only when the current + path exceeds the supported object-array editor boundary or leaves the shared schema subset. Minimal adoption checklist: @@ -117,6 +119,8 @@ Minimal adoption checklist: - 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, setting it to `true`, or mixing in `patternProperties`, `propertyNames`, or `unevaluatedProperties` is outside the supported subset +- Keep arrays on one object-valued `items` schema; tuple or open-array keywords such as `prefixItems`, + `additionalItems`, and `unevaluatedItems` are outside the supported subset Use raw YAML directly when you need: @@ -126,6 +130,7 @@ Use raw YAML directly when you need: - `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML - schema designs outside the current shared subset, including `oneOf`, `anyOf`, non-`false` `additionalProperties`, or other open-object keywords such as `patternProperties`, `propertyNames`, and `unevaluatedProperties` +- tuple or open-array designs that depend on `prefixItems`, `additionalItems`, or `unevaluatedItems` ## Documentation @@ -136,12 +141,14 @@ Use raw YAML directly when you need: - Multi-root workspaces use the first workspace folder - Validation only covers the repository's current schema subset -- Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still - fall back to raw YAML +- Form preview supports nested objects, object arrays, and nested object arrays inside object-array items as long as + those nested items still stay within the shared subset's object/scalar/array shape - Batch editing remains limited to top-level scalar fields and top-level scalar arrays - 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` +- Array-shape support is limited to one object-valued `items` schema; tuple or open-array keywords such as + `prefixItems`, `additionalItems`, and `unevaluatedItems` are rejected on purpose ## Local Testing diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 60e31f8e..7e05f709 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -1107,6 +1107,7 @@ function parseSchemaNode(rawNode, displayPath) { } validateUnsupportedOpenObjectKeyword(value, displayPath); + validateUnsupportedArrayShapeKeyword(value, displayPath); const type = resolveSupportedSchemaType(value.type, displayPath); const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath); @@ -1290,6 +1291,24 @@ function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) { "The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed."); } +/** + * Reject tuple-array and open-array keywords that would let tooling accept item + * shapes outside the Runtime / Generator shared subset. + * + * @param {Record} schemaNode Raw schema object. + * @param {string} displayPath Logical property path. + */ +function validateUnsupportedArrayShapeKeyword(schemaNode, displayPath) { + const unsupportedKeyword = getUnsupportedArrayShapeKeywordName(schemaNode); + if (!unsupportedKeyword) { + return; + } + + throw new Error( + `Schema property '${displayPath}' uses unsupported '${unsupportedKeyword}' metadata. ` + + "The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling."); +} + /** * Parse one required array child schema while keeping tooling errors aligned * with the Runtime and Source Generator contracts. @@ -1406,6 +1425,29 @@ function getUnsupportedOpenObjectKeywordName(schemaNode) { return undefined; } +/** + * Return the first array-shape keyword that the current shared schema subset + * intentionally rejects to keep array item contracts aligned. + * + * @param {Record} schemaNode Raw schema object. + * @returns {string | undefined} Unsupported keyword name when present. + */ +function getUnsupportedArrayShapeKeywordName(schemaNode) { + if (Object.prototype.hasOwnProperty.call(schemaNode, "prefixItems")) { + return "prefixItems"; + } + + if (Object.prototype.hasOwnProperty.call(schemaNode, "additionalItems")) { + return "additionalItems"; + } + + if (Object.prototype.hasOwnProperty.call(schemaNode, "unevaluatedItems")) { + return "unevaluatedItems"; + } + + 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 dd9bd723..13526ea3 100644 --- a/tools/gframework-config-tool/test/configValidation.test.js +++ b/tools/gframework-config-tool/test/configValidation.test.js @@ -302,6 +302,61 @@ test("parseSchemaContent should reject unsupported open-object keywords", () => /unsupported 'unevaluatedProperties' metadata/u); }); +test("parseSchemaContent should reject unsupported array-shape keywords", () => { + assert.throws( + () => parseSchemaContent(` + { + "type": "object", + "properties": { + "dropRates": { + "type": "array", + "prefixItems": [ + { "type": "integer" } + ], + "items": { + "type": "integer" + } + } + } + } + `), + /unsupported 'prefixItems' metadata/u); + + assert.throws( + () => parseSchemaContent(` + { + "type": "object", + "properties": { + "dropRates": { + "type": "array", + "additionalItems": false, + "items": { + "type": "integer" + } + } + } + } + `), + /unsupported 'additionalItems' metadata/u); + + assert.throws( + () => parseSchemaContent(` + { + "type": "object", + "properties": { + "dropRates": { + "type": "array", + "unevaluatedItems": false, + "items": { + "type": "integer" + } + } + } + } + `), + /unsupported 'unevaluatedItems' metadata/u); +}); + test("parseSchemaContent should reject unsupported explicit schema types", () => { assert.throws( () => parseSchemaContent(`