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

Feat/添加数组形状关键字的验证与拒绝机制
This commit is contained in:
gewuyou 2026-05-09 09:17:55 +08:00 committed by GitHub
commit 3fbc563d59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 480 additions and 18 deletions

View File

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

View File

@ -237,6 +237,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateUnsupportedArrayShapeKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
@ -916,6 +917,40 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
out diagnostic);
}
/// <summary>
/// 递归拒绝当前共享子集尚未支持的数组形状关键字。
/// 当前配置系统只接受单个 object-typed <c>items</c> schema
/// 并继续拒绝 tuple / open-array 关键字,避免生成器对数组元素形状
/// 比运行时与工具链更宽松。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>当前节点树是否未声明不支持的数组形状关键字。</returns>
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);
}
/// <summary>
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
/// </summary>
@ -976,6 +1011,36 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return false;
}
/// <summary>
/// 验证当前节点是否声明了当前共享子集尚未支持的数组形状关键字。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="element">当前 schema 节点。</param>
/// <param name="diagnostic">失败时返回的诊断。</param>
/// <returns>未声明不支持关键字时返回 <see langword="true" />。</returns>
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;
}
/// <summary>
/// 返回当前节点声明的首个未支持组合关键字。
/// </summary>
@ -1007,6 +1072,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
null;
}
/// <summary>
/// 返回当前节点声明的首个未支持数组形状关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element)
{
return element.TryGetProperty("prefixItems", out _) ? "prefixItems" :
element.TryGetProperty("additionalItems", out _) ? "additionalItems" :
element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" :
null;
}
/// <summary>
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /

View File

@ -187,4 +187,15 @@ public static class ConfigSchemaDiagnostics
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
/// <summary>
/// schema 节点声明了当前共享子集尚未支持的数组形状关键字。
/// </summary>
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);
}

View File

@ -1636,6 +1636,68 @@ public class YamlConfigLoaderTests
});
}
/// <summary>
/// 验证数组字段声明 tuple / open-array 关键字时,会在 schema 解析阶段被显式拒绝。
/// </summary>
/// <param name="keywordName">待验证的数组形状关键字名称。</param>
/// <param name="keywordValueJson">用于拼接测试 schema 的关键字值 JSON 片段。</param>
[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<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
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("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));
});
}
/// <summary>
/// 验证对象数组的 <c>contains</c> 试匹配会按声明属性子集工作,而不会因额外字段误判为不匹配。
/// </summary>

View File

@ -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));
}
/// <summary>
/// 显式拒绝当前共享子集中尚未支持的数组形状关键字。
/// 当前配置系统只接受单个 object-valued <c>items</c> schema
/// 并继续拒绝 tuple / open-array 关键字,避免 Runtime / Generator / Tooling
/// 对数组元素形状产生静默漂移。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
/// <param name="element">当前 schema 节点。</param>
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));
}
/// <summary>
/// 返回当前节点声明的首个未支持组合关键字。
/// </summary>
@ -432,6 +463,19 @@ internal static partial class YamlConfigSchemaValidator
null;
}
/// <summary>
/// 返回当前节点声明的首个未支持数组形状关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element)
{
return element.TryGetProperty("prefixItems", out _) ? "prefixItems" :
element.TryGetProperty("additionalItems", out _) ? "additionalItems" :
element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" :
null;
}
/// <summary>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </summary>

View File

@ -2017,6 +2017,56 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证生成器会显式拒绝当前共享子集尚未支持的数组形状关键字。
/// </summary>
/// <param name="keywordName">待验证的数组形状关键字名称。</param>
/// <param name="keywordValueJson">用于拼接测试 schema 的关键字值 JSON 片段。</param>
[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"));
});
}
/// <summary>
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
/// </summary>

View File

@ -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 后续改为非阻塞并行 laneactive 入口只保留主线恢复点,把批处理细节下沉到 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` schemaRuntime / Generator / Tooling 现统一拒绝 `prefixItems``additionalItems``unevaluatedItems`,并补齐三端回归测试与 reader-facing 文档说明;细节与验证命令保留在 trace 的 `2026-05-08` 记录中
- 最近验证摘要:`2026-05-09` 已按 PR `#343` latest unresolved review threads 补齐两个 public 参数化测试的 XML `<param>` 注释,并为 `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 不再重复展开逐条命令历史

View File

@ -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 ConditionTooling 文档与实际编辑边界对齐)
- 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 充足
### 验证
### 验证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 ConditionTooling 文档与实际编辑边界对齐)
- Batch baseline`origin/main` (`c01abac0`, `2026-05-06 09:40:08 +0800`)
- Primary metricbranch 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-upAI-FIRST-CONFIG-RP-003
- 已用 `gframework-pr-review` 重新抓取当前分支 PR `#343` 的 latest unresolved review threads
- 本轮只处理本地仍成立的 3 条 open threads
- `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs` 的 public 参数化测试补齐 XML `<param>` 注释
- `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs` 的 public 参数化测试补齐 XML `<param>` 注释
- `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 噪音

View File

@ -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` 条目不会把新字段并回父对象。

View File

@ -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-tableUI 中显示为 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` 这类会改变生成形状的关键字会被明确拒绝
因此,最稳妥的理解方式是:

View File

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

View File

@ -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<string, unknown>} 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<string, unknown>} 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.

View File

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