diff --git a/GFramework.Core.Tests/Ioc/AliasAwareService.cs b/GFramework.Core.Tests/Ioc/AliasAwareService.cs new file mode 100644 index 00000000..65de6dd1 --- /dev/null +++ b/GFramework.Core.Tests/Ioc/AliasAwareService.cs @@ -0,0 +1,8 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 同时实现多个别名接口的测试服务。 +/// +public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService +{ +} diff --git a/GFramework.Core.Tests/Ioc/IMixedService.cs b/GFramework.Core.Tests/Ioc/IMixedService.cs new file mode 100644 index 00000000..0a788f9b --- /dev/null +++ b/GFramework.Core.Tests/Ioc/IMixedService.cs @@ -0,0 +1,9 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 混合服务接口(用于测试优先级和非优先级混合) +/// +public interface IMixedService +{ + string? Name { get; set; } +} diff --git a/GFramework.Core.Tests/Ioc/IPrimaryAliasService.cs b/GFramework.Core.Tests/Ioc/IPrimaryAliasService.cs new file mode 100644 index 00000000..c8937b90 --- /dev/null +++ b/GFramework.Core.Tests/Ioc/IPrimaryAliasService.cs @@ -0,0 +1,6 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 主服务别名接口。 +/// +public interface IPrimaryAliasService : ISharedAliasService; diff --git a/GFramework.Core.Tests/Ioc/IPrioritizedService.cs b/GFramework.Core.Tests/Ioc/IPrioritizedService.cs new file mode 100644 index 00000000..b0998c0d --- /dev/null +++ b/GFramework.Core.Tests/Ioc/IPrioritizedService.cs @@ -0,0 +1,11 @@ +using GFramework.Core.Abstractions.Bases; + +namespace GFramework.Core.Tests.Ioc; + +/// +/// 优先级服务接口 +/// +public interface IPrioritizedService : IPrioritized +{ + string? Name { get; set; } +} diff --git a/GFramework.Core.Tests/Ioc/ISecondaryAliasService.cs b/GFramework.Core.Tests/Ioc/ISecondaryAliasService.cs new file mode 100644 index 00000000..215cbeef --- /dev/null +++ b/GFramework.Core.Tests/Ioc/ISecondaryAliasService.cs @@ -0,0 +1,6 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 次级兼容别名接口。 +/// +public interface ISecondaryAliasService : ISharedAliasService; diff --git a/GFramework.Core.Tests/Ioc/IService.cs b/GFramework.Core.Tests/Ioc/IService.cs new file mode 100644 index 00000000..b9559fef --- /dev/null +++ b/GFramework.Core.Tests/Ioc/IService.cs @@ -0,0 +1,6 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 服务接口定义 +/// +public interface IService; diff --git a/GFramework.Core.Tests/Ioc/ISharedAliasService.cs b/GFramework.Core.Tests/Ioc/ISharedAliasService.cs new file mode 100644 index 00000000..e12425bc --- /dev/null +++ b/GFramework.Core.Tests/Ioc/ISharedAliasService.cs @@ -0,0 +1,6 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 用于验证未冻结查询路径中的服务别名去重行为。 +/// +public interface ISharedAliasService; diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 19c59dcb..13fb782e 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,5 +1,4 @@ using System.Reflection; -using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; @@ -734,74 +733,3 @@ public class MicrosoftDiContainerTests Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30)); } } - -/// -/// 服务接口定义 -/// -public interface IService; - -/// -/// 测试服务类,实现 IService 接口 -/// -public sealed class TestService : IService -{ - /// - /// 获取或设置优先级 - /// - public int Priority { get; set; } -} - -/// -/// 优先级服务接口 -/// -public interface IPrioritizedService : IPrioritized -{ - string? Name { get; set; } -} - -/// -/// 混合服务接口(用于测试优先级和非优先级混合) -/// -public interface IMixedService -{ - string? Name { get; set; } -} - -/// -/// 用于验证未冻结查询路径中的服务别名去重行为。 -/// -public interface ISharedAliasService; - -/// -/// 主服务别名接口。 -/// -public interface IPrimaryAliasService : ISharedAliasService; - -/// -/// 次级兼容别名接口。 -/// -public interface ISecondaryAliasService : ISharedAliasService; - -/// -/// 同时实现多个别名接口的测试服务。 -/// -public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService -{ -} - -/// -/// 实现优先级的服务 -/// -public sealed class PrioritizedService : IPrioritizedService, IMixedService -{ - public int Priority { get; set; } - public string? Name { get; set; } -} - -/// -/// 不实现优先级的服务 -/// -public sealed class NonPrioritizedService : IMixedService -{ - public string? Name { get; set; } -} diff --git a/GFramework.Core.Tests/Ioc/NonPrioritizedService.cs b/GFramework.Core.Tests/Ioc/NonPrioritizedService.cs new file mode 100644 index 00000000..7f4f87d9 --- /dev/null +++ b/GFramework.Core.Tests/Ioc/NonPrioritizedService.cs @@ -0,0 +1,12 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 不实现优先级的服务 +/// +public sealed class NonPrioritizedService : IMixedService +{ + /// + /// 获取或设置服务名称 + /// + public string? Name { get; set; } +} diff --git a/GFramework.Core.Tests/Ioc/PrioritizedService.cs b/GFramework.Core.Tests/Ioc/PrioritizedService.cs new file mode 100644 index 00000000..da06740b --- /dev/null +++ b/GFramework.Core.Tests/Ioc/PrioritizedService.cs @@ -0,0 +1,17 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 实现优先级的服务 +/// +public sealed class PrioritizedService : IPrioritizedService, IMixedService +{ + /// + /// 获取或设置优先级 + /// + public int Priority { get; set; } + + /// + /// 获取或设置服务名称 + /// + public string? Name { get; set; } +} diff --git a/GFramework.Core.Tests/Ioc/TestService.cs b/GFramework.Core.Tests/Ioc/TestService.cs new file mode 100644 index 00000000..21dc1faa --- /dev/null +++ b/GFramework.Core.Tests/Ioc/TestService.cs @@ -0,0 +1,12 @@ +namespace GFramework.Core.Tests.Ioc; + +/// +/// 测试服务类,实现 IService 接口 +/// +public sealed class TestService : IService +{ + /// + /// 获取或设置优先级 + /// + public int Priority { get; set; } +} diff --git a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs index a7312baa..305c5f0d 100644 --- a/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs +++ b/GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs @@ -6,7 +6,6 @@ using GFramework.Core.Environment; using GFramework.Core.Events; using GFramework.Core.Ioc; using GFramework.Core.Query; -using GFramework.Cqrs.Abstractions.Cqrs.Query; namespace GFramework.Core.Tests.Query; @@ -236,179 +235,3 @@ public class AbstractAsyncQueryTests Assert.That(result2, Is.EqualTo(40)); } } - -/// -/// 测试用异步查询输入类V2 -/// -public sealed class TestAsyncQueryInputV2 : IQueryInput -{ - /// - /// 获取或设置值 - /// - public int Value { get; init; } -} - -/// -/// 整数类型测试异步查询类V4,继承AbstractAsyncQuery -/// -public sealed class TestAsyncQueryV4 : AbstractAsyncQuery -{ - /// - /// 初始化TestAsyncQueryV4的新实例 - /// - /// 查询输入参数 - public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input) - { - } - - /// - /// 获取查询是否已执行 - /// - public bool Executed { get; private set; } - - /// - /// 执行异步查询操作的具体实现 - /// - /// 查询输入参数 - /// 查询结果,将输入值乘以2 - protected override Task OnDoAsync(TestAsyncQueryInputV2 input) - { - Executed = true; - return Task.FromResult(input.Value * 2); - } -} - -/// -/// 字符串类型测试异步查询类V4,继承AbstractAsyncQuery -/// -public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery -{ - /// - /// 初始化TestAsyncStringQueryV4的新实例 - /// - /// 查询输入参数 - public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input) - { - } - - /// - /// 获取查询是否已执行 - /// - public bool Executed { get; private set; } - - /// - /// 执行异步查询操作的具体实现 - /// - /// 查询输入参数 - /// 格式化的字符串结果 - protected override Task OnDoAsync(TestAsyncQueryInputV2 input) - { - Executed = true; - return Task.FromResult($"Value: {input.Value * 2}"); - } -} - -/// -/// 复杂对象类型测试异步查询类V4,继承AbstractAsyncQuery -/// -public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery -{ - /// - /// 初始化TestAsyncComplexQueryV4的新实例 - /// - /// 查询输入参数 - public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input) - { - } - - /// - /// 获取查询是否已执行 - /// - public bool Executed { get; private set; } - - /// - /// 执行异步查询操作的具体实现 - /// - /// 查询输入参数 - /// 复杂对象查询结果 - protected override Task OnDoAsync(TestAsyncQueryInputV2 input) - { - Executed = true; - var result = new TestAsyncQueryResultV2 - { - Value = input.Value * 2, - DoubleValue = input.Value * 3 - }; - return Task.FromResult(result); - } -} - -/// -/// 测试用异步查询类(抛出异常) -/// -public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery -{ - /// - /// 初始化TestAsyncQueryWithExceptionV4的新实例 - /// - /// 查询输入参数 - public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input) - { - } - - /// - /// 执行异步查询操作并抛出异常 - /// - /// 查询输入参数 - /// 总是抛出异常 - protected override Task OnDoAsync(TestAsyncQueryInputV2 input) - { - throw new InvalidOperationException("Test exception"); - } -} - -/// -/// 测试用异步查询子类V4,继承AbstractAsyncQuery -/// -public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery -{ - /// - /// 初始化TestAsyncQueryChildV4的新实例 - /// - /// 查询输入参数 - public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input) - { - } - - /// - /// 获取查询是否已执行 - /// - public bool Executed { get; private set; } - - /// - /// 执行异步查询操作的具体实现(子类实现,乘以3) - /// - /// 查询输入参数 - /// 查询结果,将输入值乘以3 - protected override Task OnDoAsync(TestAsyncQueryInputV2 input) - { - Executed = true; - return Task.FromResult(input.Value * 3); - } -} - -/// -/// 测试用复杂查询结果类V2 -/// -public sealed class TestAsyncQueryResultV2 -{ - /// - /// 获取或设置值 - /// - public int Value { get; init; } - - /// - /// 获取或设置双倍值 - /// - public int DoubleValue { get; init; } -} diff --git a/GFramework.Core.Tests/Query/TestAsyncComplexQueryV4.cs b/GFramework.Core.Tests/Query/TestAsyncComplexQueryV4.cs new file mode 100644 index 00000000..a5661efb --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncComplexQueryV4.cs @@ -0,0 +1,38 @@ +using GFramework.Core.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 复杂对象类型测试异步查询类V4,继承AbstractAsyncQuery +/// +public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery +{ + /// + /// 初始化TestAsyncComplexQueryV4的新实例 + /// + /// 查询输入参数 + public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input) + { + } + + /// + /// 获取查询是否已执行 + /// + public bool Executed { get; private set; } + + /// + /// 执行异步查询操作的具体实现 + /// + /// 查询输入参数 + /// 复杂对象查询结果 + protected override Task OnDoAsync(TestAsyncQueryInputV2 input) + { + Executed = true; + var result = new TestAsyncQueryResultV2 + { + Value = input.Value * 2, + DoubleValue = input.Value * 3 + }; + return Task.FromResult(result); + } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncQueryChildV4.cs b/GFramework.Core.Tests/Query/TestAsyncQueryChildV4.cs new file mode 100644 index 00000000..f931a52f --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncQueryChildV4.cs @@ -0,0 +1,33 @@ +using GFramework.Core.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 测试用异步查询子类V4,继承AbstractAsyncQuery +/// +public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery +{ + /// + /// 初始化TestAsyncQueryChildV4的新实例 + /// + /// 查询输入参数 + public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input) + { + } + + /// + /// 获取查询是否已执行 + /// + public bool Executed { get; private set; } + + /// + /// 执行异步查询操作的具体实现(子类实现,乘以3) + /// + /// 查询输入参数 + /// 查询结果,将输入值乘以3 + protected override Task OnDoAsync(TestAsyncQueryInputV2 input) + { + Executed = true; + return Task.FromResult(input.Value * 3); + } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncQueryInputV2.cs b/GFramework.Core.Tests/Query/TestAsyncQueryInputV2.cs new file mode 100644 index 00000000..cba1d6d1 --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncQueryInputV2.cs @@ -0,0 +1,14 @@ +using GFramework.Cqrs.Abstractions.Cqrs.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 测试用异步查询输入类V2 +/// +public sealed class TestAsyncQueryInputV2 : IQueryInput +{ + /// + /// 获取或设置值 + /// + public int Value { get; init; } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncQueryResultV2.cs b/GFramework.Core.Tests/Query/TestAsyncQueryResultV2.cs new file mode 100644 index 00000000..068b2a66 --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncQueryResultV2.cs @@ -0,0 +1,17 @@ +namespace GFramework.Core.Tests.Query; + +/// +/// 测试用复杂查询结果类V2 +/// +public sealed class TestAsyncQueryResultV2 +{ + /// + /// 获取或设置值 + /// + public int Value { get; init; } + + /// + /// 获取或设置双倍值 + /// + public int DoubleValue { get; init; } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncQueryV4.cs b/GFramework.Core.Tests/Query/TestAsyncQueryV4.cs new file mode 100644 index 00000000..a01e42c1 --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncQueryV4.cs @@ -0,0 +1,33 @@ +using GFramework.Core.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 整数类型测试异步查询类V4,继承AbstractAsyncQuery +/// +public sealed class TestAsyncQueryV4 : AbstractAsyncQuery +{ + /// + /// 初始化TestAsyncQueryV4的新实例 + /// + /// 查询输入参数 + public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input) + { + } + + /// + /// 获取查询是否已执行 + /// + public bool Executed { get; private set; } + + /// + /// 执行异步查询操作的具体实现 + /// + /// 查询输入参数 + /// 查询结果,将输入值乘以2 + protected override Task OnDoAsync(TestAsyncQueryInputV2 input) + { + Executed = true; + return Task.FromResult(input.Value * 2); + } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs b/GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs new file mode 100644 index 00000000..f321d756 --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs @@ -0,0 +1,27 @@ +using GFramework.Core.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 测试用异步查询类(抛出异常) +/// +public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery +{ + /// + /// 初始化TestAsyncQueryWithExceptionV4的新实例 + /// + /// 查询输入参数 + public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input) + { + } + + /// + /// 执行异步查询操作并抛出异常 + /// + /// 查询输入参数 + /// 总是抛出异常 + protected override Task OnDoAsync(TestAsyncQueryInputV2 input) + { + throw new InvalidOperationException("Test exception"); + } +} diff --git a/GFramework.Core.Tests/Query/TestAsyncStringQueryV4.cs b/GFramework.Core.Tests/Query/TestAsyncStringQueryV4.cs new file mode 100644 index 00000000..59d313d5 --- /dev/null +++ b/GFramework.Core.Tests/Query/TestAsyncStringQueryV4.cs @@ -0,0 +1,33 @@ +using GFramework.Core.Query; + +namespace GFramework.Core.Tests.Query; + +/// +/// 字符串类型测试异步查询类V4,继承AbstractAsyncQuery +/// +public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery +{ + /// + /// 初始化TestAsyncStringQueryV4的新实例 + /// + /// 查询输入参数 + public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input) + { + } + + /// + /// 获取查询是否已执行 + /// + public bool Executed { get; private set; } + + /// + /// 执行异步查询操作的具体实现 + /// + /// 查询输入参数 + /// 格式化的字符串结果 + protected override Task OnDoAsync(TestAsyncQueryInputV2 input) + { + Executed = true; + return Task.FromResult($"Value: {input.Value * 2}"); + } +} diff --git a/GFramework.Game/Config/YamlConfigLoader.cs b/GFramework.Game/Config/YamlConfigLoader.cs index ce6d9ae3..975befe1 100644 --- a/GFramework.Game/Config/YamlConfigLoader.cs +++ b/GFramework.Game/Config/YamlConfigLoader.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using System.Globalization; +using System.Threading; using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using YamlDotNet.Serialization; @@ -472,93 +474,159 @@ public sealed class YamlConfigLoader : IConfigLoader IDeserializer deserializer, CancellationToken cancellationToken) { - var directoryPath = Path.Combine(rootPath, RelativePath); - if (!Directory.Exists(directoryPath)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConfigDirectoryNotFound, - Name, - $"Config directory '{directoryPath}' was not found for table '{Name}'.", - configDirectoryPath: directoryPath); - } - - YamlConfigSchema? schema = null; - IReadOnlyCollection referencedTableNames = Array.Empty(); - if (!string.IsNullOrEmpty(SchemaRelativePath)) - { - var schemaPath = Path.Combine(rootPath, SchemaRelativePath); - schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken) - .ConfigureAwait(false); - referencedTableNames = schema.ReferencedTableNames; - } - + var directoryPath = GetValidatedDirectoryPath(rootPath); + var schema = await LoadSchemaAsync(rootPath, cancellationToken).ConfigureAwait(false); var referenceUsages = new List(); + var values = await LoadValuesAsync( + directoryPath, + deserializer, + schema, + referenceUsages, + cancellationToken) + .ConfigureAwait(false); + return BuildLoadResult(directoryPath, schema, values, referenceUsages); + } + + private string GetValidatedDirectoryPath(string rootPath) + { + var directoryPath = Path.Combine(rootPath, RelativePath); + if (Directory.Exists(directoryPath)) + { + return directoryPath; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConfigDirectoryNotFound, + Name, + $"Config directory '{directoryPath}' was not found for table '{Name}'.", + configDirectoryPath: directoryPath); + } + + private async Task LoadSchemaAsync(string rootPath, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(SchemaRelativePath)) + { + return null; + } + + var schemaPath = Path.Combine(rootPath, SchemaRelativePath); + return await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken).ConfigureAwait(false); + } + + private async Task> LoadValuesAsync( + string directoryPath, + IDeserializer deserializer, + YamlConfigSchema? schema, + List referenceUsages, + CancellationToken cancellationToken) + { var values = new List(); - var files = Directory + foreach (var file in GetYamlFiles(directoryPath)) + { + cancellationToken.ThrowIfCancellationRequested(); + var yaml = await ReadYamlAsync(directoryPath, file, schema, cancellationToken).ConfigureAwait(false); + CollectReferenceUsages(referenceUsages, schema, file, yaml); + values.Add(DeserializeValue(deserializer, directoryPath, file, schema, yaml)); + } + + return values; + } + + private static string[] GetYamlFiles(string directoryPath) + { + return Directory .EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly) .Where(static path => path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)) .OrderBy(static path => path, StringComparer.Ordinal) .ToArray(); + } - foreach (var file in files) + private async Task ReadYamlAsync( + string directoryPath, + string file, + YamlConfigSchema? schema, + CancellationToken cancellationToken) + { + try { - cancellationToken.ThrowIfCancellationRequested(); + return await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConfigFileReadFailed, + Name, + $"Failed to read config file '{file}' for table '{Name}'.", + configDirectoryPath: directoryPath, + yamlPath: file, + schemaPath: schema?.SchemaPath, + innerException: exception); + } + } - string yaml; - try - { - yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false); - } - catch (Exception exception) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConfigFileReadFailed, - Name, - $"Failed to read config file '{file}' for table '{Name}'.", - configDirectoryPath: directoryPath, - yamlPath: file, - schemaPath: schema?.SchemaPath, - innerException: exception); - } - - if (schema != null) - { - // 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。 - referenceUsages.AddRange( - YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml)); - } - - try - { - var value = deserializer.Deserialize(yaml); - - if (value == null) - { - throw new InvalidOperationException("YAML content was deserialized to null."); - } - - values.Add(value); - } - catch (Exception exception) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.DeserializationFailed, - Name, - $"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.", - configDirectoryPath: directoryPath, - yamlPath: file, - schemaPath: schema?.SchemaPath, - detail: $"Target CLR type: {typeof(TValue).FullName}.", - innerException: exception); - } + private void CollectReferenceUsages( + List referenceUsages, + YamlConfigSchema? schema, + string file, + string yaml) + { + if (schema == null) + { + return; } + // 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。 + referenceUsages.AddRange( + YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml)); + } + + private TValue DeserializeValue( + IDeserializer deserializer, + string directoryPath, + string file, + YamlConfigSchema? schema, + string yaml) + { + try + { + var value = deserializer.Deserialize(yaml); + if (value != null) + { + return value; + } + + throw new InvalidOperationException("YAML content was deserialized to null."); + } + catch (Exception exception) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.DeserializationFailed, + Name, + $"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.", + configDirectoryPath: directoryPath, + yamlPath: file, + schemaPath: schema?.SchemaPath, + detail: $"Target CLR type: {typeof(TValue).FullName}.", + innerException: exception); + } + } + + private YamlTableLoadResult BuildLoadResult( + string directoryPath, + YamlConfigSchema? schema, + List values, + List referenceUsages) + { try { var table = new InMemoryConfigTable(values, _keySelector, _comparer); - return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages); + return new YamlTableLoadResult( + Name, + table, + schema?.ReferencedTableNames ?? Array.Empty(), + referenceUsages); } catch (Exception exception) { @@ -630,6 +698,12 @@ public sealed class YamlConfigLoader : IConfigLoader /// private static class CrossTableReferenceValidator { + private delegate bool IntegerTryParseDelegate( + string value, + NumberStyles style, + IFormatProvider? provider, + out T result); + /// /// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。 /// @@ -754,59 +828,15 @@ public sealed class YamlConfigLoader : IConfigLoader convertedKey = null; errorMessage = string.Empty; - if (targetKeyType == typeof(int) && - int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (TryConvertIntegerKey(rawValue, targetKeyType, typeof(int), int.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(long), long.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(short), short.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(byte), byte.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(uint), uint.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(ulong), ulong.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(ushort), ushort.TryParse, out convertedKey) || + TryConvertIntegerKey(rawValue, targetKeyType, typeof(sbyte), sbyte.TryParse, out convertedKey)) { - convertedKey = intValue; - return true; - } - - if (targetKeyType == typeof(long) && - long.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - convertedKey = longValue; - return true; - } - - if (targetKeyType == typeof(short) && - short.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var shortValue)) - { - convertedKey = shortValue; - return true; - } - - if (targetKeyType == typeof(byte) && - byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var byteValue)) - { - convertedKey = byteValue; - return true; - } - - if (targetKeyType == typeof(uint) && - uint.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uintValue)) - { - convertedKey = uintValue; - return true; - } - - if (targetKeyType == typeof(ulong) && - ulong.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ulongValue)) - { - convertedKey = ulongValue; - return true; - } - - if (targetKeyType == typeof(ushort) && - ushort.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ushortValue)) - { - convertedKey = ushortValue; - return true; - } - - if (targetKeyType == typeof(sbyte) && - sbyte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sbyteValue)) - { - convertedKey = sbyteValue; return true; } @@ -815,6 +845,25 @@ public sealed class YamlConfigLoader : IConfigLoader return false; } + private static bool TryConvertIntegerKey( + string rawValue, + Type targetKeyType, + Type supportedType, + IntegerTryParseDelegate tryParse, + out object? convertedKey) + where T : struct + { + convertedKey = null; + if (targetKeyType != supportedType || + !tryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue)) + { + return false; + } + + convertedKey = parsedValue; + return true; + } + private static bool ContainsKey(IConfigTable table, object key) { var tableInterface = table.GetType() @@ -838,7 +887,13 @@ public sealed class YamlConfigLoader : IConfigLoader new(StringComparer.Ordinal); private readonly IDeserializer _deserializer; +#if NET9_0_OR_GREATER + // net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。 + private readonly Lock _gate = new(); +#else + // net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。 private readonly object _gate = new(); +#endif private readonly Action? _onTableReloaded; private readonly Action? _onTableReloadFailed; private readonly Dictionary _registrations = new(StringComparer.Ordinal); @@ -1121,7 +1176,7 @@ public sealed class YamlConfigLoader : IConfigLoader foreach (var dependency in _dependenciesByTable) { - if (!dependency.Value.Contains(currentTableName)) + if (!ContainsDependency(dependency.Value, currentTableName)) { continue; } @@ -1138,6 +1193,14 @@ public sealed class YamlConfigLoader : IConfigLoader .ToArray(); } + private static bool ContainsDependency( + IReadOnlyCollection dependencies, + string tableName) + { + return dependencies.Any( + dependency => string.Equals(dependency, tableName, StringComparison.Ordinal)); + } + private void InvokeReloaded(string tableName) { if (_onTableReloaded == null) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 67fa6d19..2227a3d0 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,54 +6,48 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-081` -- 当前阶段:`Phase 81` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-083` +- 当前阶段:`Phase 83` - 当前焦点: - - `2026-04-27` 已复核 PR `#295` 的 latest-head review,确认 `ThrowShouldNotRetry` 的 `ParamName` open thread 属于 stale finding,本地代码已经使用传入值而非 `nameof(parameterName)` - - 已清理 `AsyncExtensionsTests.WithRetry_Should_Respect_ShouldRetry_Predicate` 中的冗余 `Task.Delay(50)`,保留 `ParamName == nameof(taskFactory)` 断言锁定契约 - - 已增强 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 failed-test 表格解析,允许 `Name` / `Failure Message` 后出现尾随额外列 - - 已新增 Python `unittest` 回归用例覆盖“尾随额外列不影响前两列提取”的场景 - - 当前剩余 warning 热点仍集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review follow-up 的低风险边界 + - `2026-04-27` 主线程已修复 `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0051`、`MA0002` 与 `MA0158`,当前非增量仓库根构建已不再报告该文件 warning + - 并行 worker 已将 `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件 + - 已接受第二波 worker 的已落地结果:`GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾辅助类型已拆分到 `Query/` 同目录独立文件 + - 最新 non-incremental 仓库根基线已从 `397` 条 warning / `316` 个唯一位点降到 `353` 条 warning / `279` 个唯一位点 + - 当前剩余 warning 热点仍集中在 `GFramework.Cqrs.Tests/Mediator/*` 的大体量 `MA0048`、以及 `YamlConfigSchemaValidator*` 等高耦合 slice ## 当前活跃事实 -- 当前 `origin/main` 基线提交为 `617e0bf`(`2026-04-26T12:17:15+08:00`)。 -- 当前 PR review 真值: - - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output ` - - 最新结果:成功;当前分支对应 PR 为 `#295` - - 当前测试报告输出已能显示 `Summary` 统计、失败测试名称,以及 `Name / Failure Message` 表格中的关键信息 - - 当前 GitHub latest-head review 仍显示 `1` 条 open thread,但该线程指向的 `nameof(parameterName)` 问题已不在本地代码中成立,属于 stale finding - - 当前 latest review 中仍有 `2` 条与本地工作树一致的 nitpick:`AsyncExtensionsTests` 冗余等待,以及 failed-test 表格解析对尾随列不鲁棒 +- 当前 `origin/main` 基线提交为 `b6a9fef`(`2026-04-27T10:53:34+08:00`)。 - 当前直接验证结果: - - `python3 .agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py` - - 最新结果:成功;`Ran 1 test in 0.000s`, `OK` - - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-postfix.json` - - 最新结果:成功;真实 PR 评论抓取仍能输出 `2` 份测试报告,失败用例详情保持可见 - - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~WithRetry_Should_Respect_ShouldRetry_Predicate"` - - 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1` - - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"` - - 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1` + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 最新结果:成功;`111 Warning(s)`、`0 Error(s)`,其中不再包含 `GFramework.Game/Config/YamlConfigLoader.cs` 的 warning + - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` + - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet clean` + - 最新结果:成功;为本轮最终 warning 基线刷新提供非增量起点 + - `dotnet build` + - 最新结果:成功;`353 Warning(s)`、`0 Error(s)`,唯一 warning 位点 `279` + - 当前构建输出已不再包含 `GFramework.Game/Config/YamlConfigLoader.cs`、`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 与 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` - 当前分支 stop-condition 指标: - - `git diff --name-only refs/remotes/origin/main...HEAD | wc -l` - - 最新结果:`35` - - `git diff --numstat refs/remotes/origin/main...HEAD` - - 最新结果:`642` changed lines + - 当前待提交工作树 footprint: + - 最新结果:`22` changed files,距离 `$gframework-batch-boot 50` 的停止线仍有余量 - 当前批次摘要: - - 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397` - - 当前批次的已完成 slice 明细已迁移到归档,active todo 仅保留恢复真值 - - 本轮新增内容为 PR review nitpick 收口与脚本回归测试补齐,不扩展 warning reduction 的热点清理边界 + - 本轮完成 `YamlConfigLoader.cs` 的单文件 warning 清理,并通过受影响模块 Release 构建验证 + - 本轮完成 `MicrosoftDiContainerTests.cs` 的 ownership-bounded `MA0048` 拆分 slice,新增 `10` 个同目录辅助类型文件并保持测试语义不变 + - 本轮还完成 `AbstractAsyncQueryTests.cs` 的 `MA0048` 拆分 slice,新增 `7` 个同目录辅助类型文件并保持测试语义不变 + - 本轮 non-incremental 仓库根 warning 真值从 `397` 降到 `353`,减少 `44` 条;唯一位点从 `316` 降到 `279`,减少 `37` 个 + - 已尝试为 `ArchitectureContextTests.cs` 启动下一波 subagent,但在共享工作树落地前已停止,不计入本轮已完成事实 - 当前建议保留到下一波次的候选: - - `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`(单点可修,但文件本身同时承载其他高耦合 warning) - - 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数) + - `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` 的 `7` 个 `MA0048` + - `GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs` 的 `7` 个 `MA0048` + - `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 与 `YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点 ## 当前风险 -- `GFramework.Game/Config/YamlConfigSchemaValidator*.cs` 仍然聚集多类高耦合 warning。 - - 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。 -- `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。 - - 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。 -- 当前 PR open thread 与 CI 失败信号仍依赖新提交进入远端 PR head 才能复核。 - - 缓解措施:本轮提交并推送后重新执行 `$gframework-pr-review`,确认 stale open thread 是否被 GitHub 收口,以及两条 nitpick 是否从 latest review 中消失。 +- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `47` / `44` / `34` 个唯一 warning 位点,属于高 changed-file 风险的 `MA0048` 大波次。 + - 缓解措施:优先继续处理 `6-7` 个 warning 的小文件切片,避免一次性推高文件数。 +- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。 + - 缓解措施:继续把它们留在独立波次,不与测试项目的低风险拆分混提。 ## 活跃文档 @@ -73,11 +67,11 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Core.Tests` 当前仍有既有 analyzer / nullable warning 基线,因此本轮验证只证明 PR review 修复未引入构建错误,未将该项目 warning 清零。 -- 后续若刷新构建或 PR review 真值,只更新上述权威区块,不在本节重复抄录。 +- `GFramework.Core.Tests` 项目级 Release 构建已在本轮清零,但仓库根 non-incremental 构建仍保留大量既有 warning。 +- warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。 ## 下一步建议 -1. 提交本轮 `AsyncExtensionsTests` / `$gframework-pr-review` nitpick 修复、Python 回归测试与 `ai-plan` 同步。 -2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 stale open thread、nitpick 与测试报告是否已刷新为新 head 真值。 -3. 若后续继续推进 warning reduction,建议另开下一波次处理 `YamlConfigLoader.cs` 热点或测试项目 `MA0048` 拆分波次。 +1. 提交本轮 `YamlConfigLoader.cs`、`MicrosoftDiContainerTests.cs`、`AbstractAsyncQueryTests.cs` 的 warning reduction 结果及 `ai-plan` 同步。 +2. 下一波优先挑选 `ArchitectureContextTests.cs` 或 `AsyncQueryExecutorTests.cs` 这类 `7`-warning 的纯 `MA0048` 单文件切片。 +3. 继续将 `YamlConfigSchemaValidator*` 与 `GFramework.Cqrs.Tests/Mediator/*` 作为独立高风险波次处理。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 9c8bc443..efb1d296 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,39 +1,45 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-27 — RP-081 +## 2026-04-27 — RP-083 -### 阶段:核实 PR `#295` 的剩余 nitpick,并补齐脚本解析回归测试 +### 阶段:修复 `YamlConfigLoader` 单文件 warning,并拆分 `MicrosoftDiContainerTests` 的辅助类型 - 触发背景: - - 用户再次执行 `$gframework-pr-review`,需要根据当前 PR `#295` 的 latest-head review 继续核实哪些反馈仍需在本地处理 - - 远端 review 显示 `1` 条 open thread 与 `2` 条 nitpick,需要区分 stale finding 与仍然成立的本地问题 + - 用户执行 `$gframework-batch-boot 50`,要求先拿仓库根构建 warning,再按 bounded slice 分派给不同 subagent 并持续推进 + - 当前分支在本轮开始时与 `origin/main@b6a9fef` 零提交差异,适合从低风险 warning slice 起步 - 主线程实施: - - 复核 `/tmp/current-pr-review.json` 与本地 `AsyncExtensionsTests.cs`,确认 open thread 指向的 `nameof(parameterName)` 问题已在现有代码中修复,属于 stale finding - - 删除 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 中 `WithRetry_Should_Respect_ShouldRetry_Predicate` 的冗余 `Task.Delay(50)`,将测试改回同步断言路径 - - 调整 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 `parse_failed_test_details`,允许 failed-test HTML 表格在 `Name` / `Failure Message` 后追加额外列 - - 新增 `.agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py`,以 `unittest` 覆盖“尾随额外列不影响前两列提取”的回归场景 + - 先执行 non-incremental 仓库根基线:`dotnet clean` + `dotnet build`,得到 `397 Warning(s)` / `316` 个唯一位点 + - 主线程修复 `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0051`、`MA0002` 与 `MA0158` + - 接受一个 worker batch:将 `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件 + - 接受第二波 worker 的已落地结果:将 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾的 `7` 个测试辅助类型拆分到 `Query/` 同目录独立文件 + - 启动 `ArchitectureContextTests.cs` 候选 worker,但在共享工作树落地前主动停止,以避免本轮上下文与 review 面积继续膨胀 - 验证里程碑: - - `python3 .agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py` - - 结果:成功;`Ran 1 test in 0.000s`, `OK` - - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-postfix.json` - - 结果:成功;真实 PR 评论抓取仍显示 `2` 份测试报告,失败测试名与 failure message 摘要保持可见 - - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~WithRetry_Should_Respect_ShouldRetry_Predicate"` - - 结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1` + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功;`111 Warning(s)`、`0 Error(s)` + - 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs` + - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet clean` + - 结果:成功;刷新最终 non-incremental 仓库根 warning 基线 + - `dotnet build` + - 结果:成功;`353 Warning(s)`、`0 Error(s)`,唯一位点 `279` + - 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs`、`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 与 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` - 当前结论: - - 本轮 latest-head review 中只有 `AsyncExtensionsTests` 冗余等待与 failed-test 表格尾随列容错性两个 nitpick 仍与本地代码一致,现已修复 - - `ThrowShouldNotRetry` 的 `ParamName` open thread 属于 stale finding,本地代码已经符合预期,只需等待新提交进入远端后复核 thread 状态 + - 本轮已完成一个主线程单文件 slice 和两个 worker 拆分 slice;仓库根 non-incremental warning 从 `397` 降到 `353` + - 当前共享工作树 footprint 为 `22` 个 changed files,仍低于 `$gframework-batch-boot 50` 的停止线 + - 下一波更适合继续处理 `7` 个 `MA0048` 的小文件,而不是立即进入 `Mediator*` 或 `YamlConfigSchemaValidator*` 的高耦合热点 ## 活跃风险 -- PR 上的 latest-head review thread 与测试报告仍需要等新提交进入远端后再复核。 - - 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只以新的 latest-head 和 test report 为准。 -- `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与 `MA0048` 拆分仍是下一波次的高耦合候选。 - - 缓解措施:保持本轮边界只处理 PR review nitpick follow-up,不顺手扩展 warning reduction 范围。 +- `GFramework.Cqrs.Tests/Mediator/*` 的 `MA0048` 位点密度很高,一次性拆分会迅速推高 changed-file 数。 + - 缓解措施:下一波优先继续拿 `7` warning 级别的小切片。 +- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。 + - 缓解措施:继续维持为独立波次,不与测试项目拆分混提。 ## 下一步 -1. 完成本轮提交。 -2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 stale open thread 与 nitpick 是否已刷新。 +1. 完成本轮 `YamlConfigLoader.cs`、`MicrosoftDiContainerTests.cs` 与 `ai-plan` 的提交。 +2. 下一波优先从 `ArchitectureContextTests.cs` 或 `AsyncQueryExecutorTests.cs` 继续拆分纯 `MA0048`。 ## 历史归档指针