mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(game-config): 收紧开放对象关键字边界
- 修复 Runtime、Generator 与 Tooling 对 patternProperties、propertyNames、unevaluatedProperties 的静默接受风险 - 补充三端对称回归测试与 reader-facing 文档边界说明 - 更新 ai-plan 恢复点、验证记录与下一步指针
This commit is contained in:
parent
a8c6c11e9e
commit
cb6dd8a510
@ -884,7 +884,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
|
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
|
||||||
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
|
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
|
||||||
/// <c>additionalProperties: false</c>。
|
/// <c>additionalProperties: false</c>,并继续拒绝
|
||||||
|
/// <c>patternProperties</c>、<c>propertyNames</c> 与
|
||||||
|
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filePath">Schema 文件路径。</param>
|
/// <param name="filePath">Schema 文件路径。</param>
|
||||||
/// <param name="displayPath">逻辑字段路径。</param>
|
/// <param name="displayPath">逻辑字段路径。</param>
|
||||||
@ -959,12 +961,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
out Diagnostic? diagnostic)
|
out Diagnostic? diagnostic)
|
||||||
{
|
{
|
||||||
diagnostic = null;
|
diagnostic = null;
|
||||||
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
|
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
if (string.Equals(keywordName, "additionalProperties", StringComparison.Ordinal) &&
|
||||||
|
element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
|
||||||
|
additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -974,8 +978,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
CreateFileLocation(filePath),
|
CreateFileLocation(filePath),
|
||||||
Path.GetFileName(filePath),
|
Path.GetFileName(filePath),
|
||||||
displayPath,
|
displayPath,
|
||||||
"additionalProperties",
|
keywordName,
|
||||||
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
|
"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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -991,6 +995,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
null;
|
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>
|
/// <summary>
|
||||||
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
||||||
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
||||||
|
|||||||
@ -81,6 +81,9 @@ GameProject/
|
|||||||
- `oneOf`
|
- `oneOf`
|
||||||
- `anyOf`
|
- `anyOf`
|
||||||
- 非 `false` 的 `additionalProperties`
|
- 非 `false` 的 `additionalProperties`
|
||||||
|
- `patternProperties`
|
||||||
|
- `propertyNames`
|
||||||
|
- `unevaluatedProperties`
|
||||||
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束
|
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束
|
||||||
|
|
||||||
遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。
|
遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。
|
||||||
|
|||||||
@ -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>
|
/// <summary>
|
||||||
/// 验证 allOf 条目只接受 object-typed schema。
|
/// 验证 allOf 条目只接受 object-typed schema。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -373,7 +373,9 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
|
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
|
||||||
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
|
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
|
||||||
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。
|
/// <c>additionalProperties: false</c>,并继续拒绝
|
||||||
|
/// <c>patternProperties</c>、<c>propertyNames</c> 与
|
||||||
|
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tableName">所属配置表名称。</param>
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
@ -385,12 +387,14 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element)
|
JsonElement element)
|
||||||
{
|
{
|
||||||
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
|
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
if (string.Equals(keywordName, "additionalProperties", StringComparison.Ordinal) &&
|
||||||
|
element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
|
||||||
|
additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -398,8 +402,8 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
ConfigLoadFailureKind.SchemaUnsupported,
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
tableName,
|
tableName,
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported 'additionalProperties' metadata. " +
|
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
|
||||||
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.",
|
"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,
|
schemaPath: schemaPath,
|
||||||
displayPath: GetDiagnosticPath(propertyPath));
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
@ -416,6 +420,25 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
null;
|
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>
|
/// <summary>
|
||||||
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -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>
|
/// <summary>
|
||||||
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
|
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
|
||||||
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
- 已明确将 `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
|
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
||||||
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
||||||
- Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
|
- Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
|
||||||
@ -20,6 +21,8 @@
|
|||||||
|
|
||||||
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
|
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
|
||||||
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
|
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
|
||||||
|
- 开放对象形状风险:如果某一端静默接受 `patternProperties`、`propertyNames`、`unevaluatedProperties` 等关键字,会重新打开对象形状并造成契约漂移
|
||||||
|
- 缓解措施:当前三端已统一把开放对象边界收紧为只接受 `additionalProperties: false`,其余开放对象关键字直接报错
|
||||||
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
|
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
|
||||||
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
|
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
|
||||||
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
|
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
|
||||||
@ -40,6 +43,9 @@
|
|||||||
- `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
- `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
||||||
- 已明确拒绝会改变生成类型形状的组合关键字:
|
- 已明确拒绝会改变生成类型形状的组合关键字:
|
||||||
- `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
|
- `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
|
||||||
|
- 已明确拒绝会重新打开对象形状的开放对象关键字:
|
||||||
|
- 当前只接受 `additionalProperties: false`
|
||||||
|
- `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
|
||||||
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
|
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
|
||||||
- 只允许 object 节点上的 object-typed inline schema
|
- 只允许 object 节点上的 object-typed inline schema
|
||||||
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
|
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
|
||||||
@ -86,11 +92,12 @@
|
|||||||
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
|
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
|
||||||
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
|
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
|
||||||
- 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
|
- 最近验证摘要:`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` 记录中
|
||||||
- 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 `#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 不再重复展开逐条命令历史
|
- 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 示例或采用路径,不再重复扩写能力边界说明
|
2. Tooling / Docs 若要并发推进,优先补 reader-facing 示例或采用路径,不再重复扩写能力边界说明
|
||||||
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针
|
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针
|
||||||
|
|||||||
@ -231,3 +231,52 @@
|
|||||||
|
|
||||||
1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复
|
1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复
|
||||||
2. 若还有残留 open threads,优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理
|
2. 若还有残留 open threads,优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理
|
||||||
|
|
||||||
|
## 2026-05-06
|
||||||
|
|
||||||
|
### 阶段:开放对象关键字边界收口(AI-FIRST-CONFIG-RP-003)
|
||||||
|
|
||||||
|
- 已在 Runtime、Source Generator 与 VS Code Tooling 三端统一收紧开放对象关键字边界
|
||||||
|
- 本轮不是扩 JSON Schema 能力,而是避免某一端静默接受会重新打开对象形状的 schema:
|
||||||
|
- 当前继续接受 `additionalProperties: false` 作为显式闭合对象提醒
|
||||||
|
- `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前改为三端直接失败
|
||||||
|
- reader-facing docs 也已同步更新,避免采用文档继续把这类关键字描述成“也许工具没做但运行时可能支持”的灰区
|
||||||
|
|
||||||
|
### 关键决定
|
||||||
|
|
||||||
|
- `additionalProperties: false` 仍是唯一共享支持的开放对象相关关键字形状
|
||||||
|
- 任何会重新引入动态字段集的开放对象关键字,都视为当前主线之外的设计,而不是后续工具增强项
|
||||||
|
- 本轮继续保持主线为 `C# Runtime + Source Generator + Consumer DX`,没有把工作重心切回复杂表单或宿主验证
|
||||||
|
|
||||||
|
### Stop Condition
|
||||||
|
|
||||||
|
- Batch baseline:`origin/main` (`a8c6c11e`, `2026-05-05 13:14:24 +0800`)
|
||||||
|
- Primary metric:branch diff vs `origin/main` changed files,阈值 `50`
|
||||||
|
- 本轮执行时的 branch diff 指标仍为 `0`,说明当前批次尚未把 `HEAD` 推进到接近阈值;reviewability headroom 充足
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- 2026-05-06:`bun run test`(`tools/gframework-config-tool`)
|
||||||
|
- 结果:通过(133 tests)
|
||||||
|
- 备注:新增 JS 回归覆盖 `patternProperties`、`propertyNames` 与 `unevaluatedProperties` 的显式拒绝
|
||||||
|
- 2026-05-06:`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderAllOfTests"`
|
||||||
|
- 结果:通过(18 tests)
|
||||||
|
- 备注:运行时新增开放对象关键字拒绝回归,继续沿用 `SchemaUnsupported` + `reward` 诊断路径
|
||||||
|
- 2026-05-06:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
|
||||||
|
- 结果:通过(57 tests)
|
||||||
|
- 备注:生成器新增 `GF_ConfigSchema_016` 对称回归,覆盖 3 类开放对象关键字
|
||||||
|
- 2026-05-06:`dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||||
|
- 结果:通过(0 warnings, 0 errors)
|
||||||
|
- 2026-05-06:`dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release`
|
||||||
|
- 结果:通过(0 warnings, 0 errors)
|
||||||
|
- 2026-05-06:`python3 scripts/license-header.py --check --paths ...`
|
||||||
|
- 结果:通过
|
||||||
|
- 备注:仓库脚本默认 `git ls-files` 在当前 WSL worktree 绑定下无法直接解析仓库上下文,因此本轮改为对受影响文件执行 targeted check
|
||||||
|
- 2026-05-06:`git diff --check`
|
||||||
|
- 结果:通过
|
||||||
|
|
||||||
|
### 下一步
|
||||||
|
|
||||||
|
1. 继续盘点下一批不会改变生成类型形状、也不会重新打开对象形状的共享关键字
|
||||||
|
2. Tooling / Docs 如继续并发推进,优先补真实采用示例,不再重复扩写开放对象边界清单
|
||||||
|
3. 若后续 batch 再触碰 schema contract,继续保持 Runtime / Generator / Tooling 三端同步失败语义与 reader-facing docs 一致
|
||||||
|
|||||||
@ -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 未声明的额外同级字段继续存在
|
- `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 语义把每个条目叠加到当前对象上,不做属性合并,也不改变生成类型形状
|
- `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 语义,允许对象保留未在条件块中声明的额外同级字段
|
- `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 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字
|
- `oneOf` / `anyOf`:当前不属于共享支持子集;Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字
|
||||||
|
|
||||||
如果你的 schema 需要超出这些边界的复杂 shape,推荐采用下面的回退顺序:
|
如果你的 schema 需要超出这些边界的复杂 shape,推荐采用下面的回退顺序:
|
||||||
|
|
||||||
1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口
|
1. 先在 raw YAML 与 schema 文件中直接编辑,而不是强行依赖表单入口
|
||||||
2. 再核对该 shape 是否仍符合这里列出的共享支持子集
|
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` 条目不会把新字段并回父对象。
|
`allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。
|
||||||
|
|
||||||
|
|||||||
@ -86,7 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer);
|
|||||||
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
|
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
|
||||||
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
|
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
|
||||||
|
|
||||||
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及
|
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`,`patternProperties` / `propertyNames` / `unevaluatedProperties` 也不属于共享子集)这类已收口的对象边界,以及
|
||||||
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
|
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
|
||||||
shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
|
shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
|
||||||
|
|
||||||
|
|||||||
@ -115,8 +115,8 @@ Minimal adoption checklist:
|
|||||||
- Place each config domain under `config/<domain>/*.yaml`
|
- Place each config domain under `config/<domain>/*.yaml`
|
||||||
- Place the matching schema at `schemas/<domain>.schema.json`
|
- 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
|
- 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
|
- Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it, setting
|
||||||
it to `true` is outside the supported subset
|
it to `true`, or mixing in `patternProperties`, `propertyNames`, or `unevaluatedProperties` is outside the supported subset
|
||||||
|
|
||||||
Use raw YAML directly when you need:
|
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
|
- 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
|
push the edit path beyond the lightweight form boundary
|
||||||
- `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML
|
- `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
|
## 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
|
- Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still
|
||||||
fall back to raw YAML
|
fall back to raw YAML
|
||||||
- Batch editing remains limited to top-level scalar fields and top-level scalar arrays
|
- 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` /
|
- Closed-object support is limited to `additionalProperties: false`; open-object keywords such as
|
||||||
`anyOf` are rejected on purpose
|
`patternProperties`, `propertyNames`, and `unevaluatedProperties` are rejected on purpose, as are unsupported
|
||||||
|
combinators such as `oneOf` / `anyOf`
|
||||||
|
|
||||||
## Local Testing
|
## Local Testing
|
||||||
|
|
||||||
|
|||||||
@ -1273,23 +1273,25 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
/**
|
/**
|
||||||
* Reject open-object keyword forms that would drift away from the Runtime and
|
* Reject open-object keyword forms that would drift away from the Runtime and
|
||||||
* Source Generator contracts. The current shared subset keeps object fields
|
* 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 {Record<string, unknown>} schemaNode Raw schema object.
|
||||||
* @param {string} displayPath Logical property path.
|
* @param {string} displayPath Logical property path.
|
||||||
*/
|
*/
|
||||||
function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
|
function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(schemaNode, "additionalProperties")) {
|
const unsupportedKeyword = getUnsupportedOpenObjectKeywordName(schemaNode);
|
||||||
|
if (!unsupportedKeyword) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.additionalProperties === false) {
|
if (unsupportedKeyword === "additionalProperties" && schemaNode.additionalProperties === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Schema property '${displayPath}' uses unsupported 'additionalProperties' metadata. ` +
|
`Schema property '${displayPath}' uses unsupported '${unsupportedKeyword}' metadata. ` +
|
||||||
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
|
"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 +1382,34 @@ function getUnsupportedCombinatorKeywordName(schemaNode) {
|
|||||||
return undefined;
|
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
|
* Parse one optional `not` sub-schema and keep path formatting aligned with
|
||||||
* the runtime/generator diagnostics.
|
* the runtime/generator diagnostics.
|
||||||
|
|||||||
@ -228,6 +228,63 @@ test("parseSchemaContent should reject unsupported additionalProperties forms",
|
|||||||
/unsupported 'additionalProperties' metadata/u);
|
/unsupported 'additionalProperties' metadata/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
test("parseSchemaContent should reject unsupported explicit schema types", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => parseSchemaContent(`
|
() => parseSchemaContent(`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user