Merge pull request #301 from GeWuYou/fix/analyzer-warning-reduction-batch

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-29 11:14:56 +08:00 committed by GitHub
commit 4557dde631
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 2896 additions and 1866 deletions

View File

@ -52,7 +52,7 @@ public class MediatorAdvancedFeaturesTests
var request = new TestValidatedRequest { Value = -1 }; // 无效值
Assert.ThrowsAsync<ArgumentException>(async () =>
await _context!.SendRequestAsync(request));
await _context!.SendRequestAsync(request).ConfigureAwait(false));
}
[Test]
@ -62,7 +62,7 @@ public class MediatorAdvancedFeaturesTests
TestRetryBehavior.AttemptCount = 0;
var request = new TestRetryRequest { ShouldFailTimes = 0 }; // 不失败
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo("Success"));
Assert.That(TestRetryBehavior.AttemptCount, Is.EqualTo(1));
@ -82,7 +82,7 @@ public class MediatorAdvancedFeaturesTests
tasks.Add(_context!.SendRequestAsync(request).AsTask());
}
var results = await Task.WhenAll(tasks);
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
stopwatch.Stop();
// 验证所有请求都成功处理
@ -102,7 +102,7 @@ public class MediatorAdvancedFeaturesTests
for (int i = 0; i < requestCount; i++)
{
var request = new TestMemoryRequest { Data = new string('x', 1000) };
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
// 定期强制GC来测试内存泄漏
if (i % 100 == 0)
@ -126,7 +126,7 @@ public class MediatorAdvancedFeaturesTests
TestTransientErrorHandler.ErrorCount = 0;
var request = new TestTransientErrorRequest { MaxErrors = 0 }; // 不出错
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo("Success"));
Assert.That(TestTransientErrorHandler.ErrorCount, Is.EqualTo(0));
@ -140,7 +140,8 @@ public class MediatorAdvancedFeaturesTests
{
try
{
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true });
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true })
.ConfigureAwait(false);
}
catch (Exception)
{
@ -151,7 +152,8 @@ public class MediatorAdvancedFeaturesTests
// 验证断路器已打开,后续请求应该快速失败
var stopwatch = Stopwatch.StartNew();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false }));
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false })
.ConfigureAwait(false));
stopwatch.Stop();
// 验证快速失败(应该在很短时间内完成)
@ -172,7 +174,7 @@ public class MediatorAdvancedFeaturesTests
// 执行saga
foreach (var request in requests)
{
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
}
// 验证所有步骤都成功执行
@ -192,10 +194,10 @@ public class MediatorAdvancedFeaturesTests
};
// 执行saga第二步会失败
await _context!.SendRequestAsync(requests[0]);
await _context!.SendRequestAsync(requests[0]).ConfigureAwait(false);
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _context.SendRequestAsync(requests[1]));
await _context.SendRequestAsync(requests[1]).ConfigureAwait(false));
// 验证回滚机制被触发
Assert.That(sagaData.CompletedSteps, Is.EqualTo(new[] { 1 })); // 只有第一步完成
@ -206,7 +208,7 @@ public class MediatorAdvancedFeaturesTests
[Test]
public async Task Request_Chaining_With_Dependencies_Should_Work_Correctly()
{
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest());
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest()).ConfigureAwait(false);
Assert.That(chainResult, Is.EqualTo("Chain completed: Step1 -> Step2 -> Step3"));
}
@ -218,7 +220,7 @@ public class MediatorAdvancedFeaturesTests
var request = new TestExternalServiceRequest { TimeoutMs = 1000 };
Assert.ThrowsAsync<TaskCanceledException>(async () =>
await _context!.SendRequestAsync(request, cts.Token));
await _context!.SendRequestAsync(request, cts.Token).ConfigureAwait(false));
}
[Test]
@ -227,13 +229,15 @@ public class MediatorAdvancedFeaturesTests
var testData = new List<string>();
var request = new TestDatabaseRequest { Data = "test data", Storage = testData };
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo("Data saved successfully"));
Assert.That(testData, Contains.Item("test data"));
}
}
// 这些高级特性测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。
#pragma warning disable MA0048
#region Advanced Test Classes
public sealed class TestRetryRequestHandler : IRequestHandler<TestRetryRequest, string>
@ -329,7 +333,7 @@ public sealed class TestChainStartRequestHandler : IRequestHandler<TestChainStar
public async ValueTask<string> Handle(TestChainStartRequest request, CancellationToken cancellationToken)
{
// 模拟链式调用
await Task.Delay(10, cancellationToken);
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
return "Chain completed: Step1 -> Step2 -> Step3";
}
}
@ -338,7 +342,7 @@ public sealed class TestExternalServiceRequestHandler : IRequestHandler<TestExte
{
public async ValueTask<string> Handle(TestExternalServiceRequest request, CancellationToken cancellationToken)
{
await Task.Delay(request.TimeoutMs, cancellationToken);
await Task.Delay(request.TimeoutMs, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
return "External service response";
}
@ -378,7 +382,7 @@ public sealed class TestValidatedRequestHandler : IRequestHandler<TestValidatedR
// 验证输入
if (request.Value < 0)
{
throw new ArgumentException("Value must be non-negative", nameof(request.Value));
throw new ArgumentException("Value must be non-negative", nameof(request));
}
return new ValueTask<string>($"Value: {request.Value}");
@ -406,7 +410,7 @@ public sealed class TestPerformanceRequestHandler : IRequestHandler<TestPerforma
{
public async ValueTask<int> Handle(TestPerformanceRequest request, CancellationToken cancellationToken)
{
await Task.Delay(request.ProcessingTimeMs, cancellationToken);
await Task.Delay(request.ProcessingTimeMs, cancellationToken).ConfigureAwait(false);
return request.Id;
}
}
@ -503,3 +507,4 @@ public sealed record TestDatabaseRequest : IRequest<string>
}
#endregion
#pragma warning restore MA0048

View File

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Architectures;
@ -62,7 +63,7 @@ public class MediatorArchitectureIntegrationTests
TestContextAwareHandler.LastContext = _context; // 直接设置
var request = new TestContextAwareRequest();
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(TestContextAwareHandler.LastContext, Is.Not.Null);
Assert.That(TestContextAwareHandler.LastContext, Is.SameAs(_context));
@ -74,7 +75,7 @@ public class MediatorArchitectureIntegrationTests
TestServiceRetrievalHandler.LastRetrievedService = null;
var request = new TestServiceRetrievalRequest();
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.Not.Null);
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.InstanceOf<TestService>());
@ -86,7 +87,7 @@ public class MediatorArchitectureIntegrationTests
TestNestedRequestHandler2.ExecutionCount = 0;
var request = new TestNestedRequest { Depth = 1 }; // 简化为深度1
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo("Nested execution completed at depth 1"));
Assert.That(TestNestedRequestHandler2.ExecutionCount, Is.EqualTo(1));
@ -99,7 +100,7 @@ public class MediatorArchitectureIntegrationTests
TestLifecycleHandler.DisposalCount = 0;
var request = new TestLifecycleRequest();
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
// 验证生命周期管理
Assert.That(TestLifecycleHandler.InitializationCount, Is.EqualTo(1));
@ -116,14 +117,14 @@ public class MediatorArchitectureIntegrationTests
.Select(async i =>
{
var request = new TestScopedServiceRequest { RequestId = i };
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
lock (results)
{
results.Add(result);
}
});
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// 验证每个请求都得到了独立的scope实例
Assert.That(results.Distinct().Count(), Is.EqualTo(10));
@ -135,7 +136,7 @@ public class MediatorArchitectureIntegrationTests
var request = new TestErrorPropagationRequest();
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _context!.SendRequestAsync(request));
await _context!.SendRequestAsync(request).ConfigureAwait(false));
Assert.That(ex!.Message, Is.EqualTo("Test error from handler"));
Assert.That(ex.Data["RequestId"], Is.Not.Null);
@ -148,7 +149,7 @@ public class MediatorArchitectureIntegrationTests
var request = new TestExceptionRequest();
Assert.ThrowsAsync<DivideByZeroException>(async () =>
await _context!.SendRequestAsync(request));
await _context!.SendRequestAsync(request).ConfigureAwait(false));
// 验证异常被捕获和记录
Assert.That(TestExceptionHandler.LastException, Is.Not.Null);
@ -164,7 +165,7 @@ public class MediatorArchitectureIntegrationTests
for (int i = 0; i < iterations; i++)
{
var request = new TestPerformanceRequest2 { Id = i };
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(i));
}
@ -188,7 +189,7 @@ public class MediatorArchitectureIntegrationTests
{
var stopwatch = Stopwatch.StartNew();
var request = new TestUncachedRequest { Id = i };
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
stopwatch.Stop();
uncachedTimes.Add(stopwatch.ElapsedMilliseconds);
}
@ -198,7 +199,7 @@ public class MediatorArchitectureIntegrationTests
{
var stopwatch = Stopwatch.StartNew();
var request = new TestCachedRequest { Id = i };
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
stopwatch.Stop();
cachedTimes.Add(stopwatch.ElapsedMilliseconds);
}
@ -224,12 +225,12 @@ public class MediatorArchitectureIntegrationTests
var task = Task.Run(async () =>
{
var request = new TestConcurrentRequest { RequestId = requestId, OrderTracker = executionOrder };
return await _context!.SendRequestAsync(request);
return await _context!.SendRequestAsync(request).ConfigureAwait(false);
});
tasks.Add(task);
}
var results = await Task.WhenAll(tasks);
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
// 验证所有请求都成功完成
Assert.That(results.Length, Is.EqualTo(concurrentRequests));
@ -253,10 +254,10 @@ public class MediatorArchitectureIntegrationTests
SharedState = sharedState,
Increment = 1
};
await _context!.SendRequestAsync(request);
await _context!.SendRequestAsync(request).ConfigureAwait(false);
});
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// 验证最终状态正确20个并发操作每个+1
Assert.That(sharedState.Counter, Is.EqualTo(concurrentOperations));
@ -269,7 +270,7 @@ public class MediatorArchitectureIntegrationTests
TestIntegrationHandler.LastSystemCall = null;
var request = new TestIntegrationRequest();
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo("Integration successful"));
Assert.That(TestIntegrationHandler.LastSystemCall, Is.EqualTo("System executed"));
@ -285,7 +286,7 @@ public class MediatorArchitectureIntegrationTests
// 使用Mediator
var mediatorRequest = new TestMediatorRequest { Value = 42 };
var result = await _context.SendRequestAsync(mediatorRequest);
var result = await _context.SendRequestAsync(mediatorRequest).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(42));
// 验证两者可以共存
@ -296,8 +297,8 @@ public class MediatorArchitectureIntegrationTests
[Test]
public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request()
{
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest());
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest());
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -306,312 +307,312 @@ public class MediatorArchitectureIntegrationTests
Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context));
});
}
}
#region Integration Test Classes
#region Integration Test Classes
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
{
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
{
// 保持测试中设置的上下文不要重置为null
return new ValueTask<string>("Context accessed");
}
}
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
{
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
{
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
return new ValueTask<string>("Service retrieved");
}
}
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
{
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
{
TestNestedRequestHandler2.ExecutionCount++;
if (request.Depth >= 1) // 简化条件
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
{
// 保持测试中设置的上下文不要重置为null
return new ValueTask<string>("Context accessed");
}
}
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
{
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
{
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
return new ValueTask<string>("Service retrieved");
}
}
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
{
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
{
TestNestedRequestHandler2.ExecutionCount++;
// 模拟嵌套调用
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
}
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
}
}
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
{
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
{
TestLifecycleHandler.InitializationCount++;
// 模拟一些工作
TestLifecycleHandler.DisposalCount++;
return new ValueTask<string>("Lifecycle managed");
}
}
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
{
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
{
// 模拟返回请求ID
return new ValueTask<int>(request.RequestId);
}
}
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
{
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
{
var ex = new InvalidOperationException("Test error from handler");
ex.Data["RequestId"] = Guid.NewGuid();
throw ex;
}
}
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
{
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
{
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
throw TestExceptionHandler.LastException;
}
}
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
{
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
{
return new ValueTask<int>(request.Id);
}
}
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
{
public ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
{
// 模拟一些处理时间
Task.Delay(5, cancellationToken).Wait(cancellationToken);
return new ValueTask<int>(request.Id);
}
}
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
{
private static readonly Dictionary<int, int> _cache = new();
public ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(request.Id, out var cachedValue))
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
{
return new ValueTask<int>(cachedValue);
TestLifecycleHandler.InitializationCount++;
// 模拟一些工作
TestLifecycleHandler.DisposalCount++;
return new ValueTask<string>("Lifecycle managed");
}
// 模拟处理时间
Task.Delay(10, cancellationToken).Wait(cancellationToken);
var newValue = request.Id;
_cache[request.Id] = newValue;
return new ValueTask<int>(newValue);
}
}
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
{
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
{
lock (request.OrderTracker)
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
{
request.OrderTracker.Add(request.RequestId);
// 模拟返回请求ID
return new ValueTask<int>(request.RequestId);
}
return new ValueTask<int>(request.RequestId);
}
}
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
{
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
{
request.SharedState.Counter += request.Increment;
return new ValueTask<string>("State modified");
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
{
var ex = new InvalidOperationException("Test error from handler");
ex.Data["RequestId"] = Guid.NewGuid();
throw ex;
}
}
}
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
{
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
{
TestIntegrationHandler.LastSystemCall = "System executed";
return new ValueTask<string>("Integration successful");
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
{
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
throw TestExceptionHandler.LastException;
}
}
}
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
{
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
{
return new ValueTask<int>(request.Value);
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
{
return new ValueTask<int>(request.Id);
}
}
}
/// <summary>
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
/// </summary>
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
IRequestHandler<TestPerDispatchContextAwareRequest, int>
{
private static int _nextInstanceId;
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
public static List<IArchitectureContext?> Contexts { get; } = [];
public static List<int> SeenInstanceIds { get; } = [];
/// <summary>
/// 记录当前实例编号与收到的架构上下文。
/// </summary>
/// <param name="request">请求实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>当前处理器实例编号。</returns>
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
{
Contexts.Add(Context);
SeenInstanceIds.Add(_instanceId);
return ValueTask.FromResult(_instanceId);
public async ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
{
// 模拟一些处理时间
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
return request.Id;
}
}
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
{
private static readonly ConcurrentDictionary<int, int> _cache = new();
public async ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(request.Id, out var cachedValue))
{
return cachedValue;
}
// 模拟处理时间
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
return _cache.GetOrAdd(request.Id, static id => id);
}
}
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
{
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
{
lock (request.OrderTracker)
{
request.OrderTracker.Add(request.RequestId);
}
return new ValueTask<int>(request.RequestId);
}
}
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
{
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
{
request.SharedState.IncrementBy(request.Increment);
return new ValueTask<string>("State modified");
}
}
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
{
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
{
TestIntegrationHandler.LastSystemCall = "System executed";
return new ValueTask<string>("Integration successful");
}
}
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
{
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
{
return new ValueTask<int>(request.Value);
}
}
/// <summary>
/// 重置跨测试共享的实例跟踪状态。
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例
/// </summary>
public static void Reset()
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
IRequestHandler<TestPerDispatchContextAwareRequest, int>
{
Contexts.Clear();
SeenInstanceIds.Clear();
_nextInstanceId = 0;
}
}
private static int _nextInstanceId;
private static readonly List<IArchitectureContext?> TrackedContexts = [];
private static readonly List<int> TrackedInstanceIds = [];
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
public sealed record TestContextAwareRequest : IRequest<string>;
public static IReadOnlyList<IArchitectureContext?> Contexts => TrackedContexts;
public static IReadOnlyList<int> SeenInstanceIds => TrackedInstanceIds;
public static class TestContextAwareHandler
{
public static IArchitectureContext? LastContext { get; set; }
}
/// <summary>
/// 记录当前实例编号与收到的架构上下文。
/// </summary>
/// <param name="request">请求实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>当前处理器实例编号。</returns>
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
{
TrackedContexts.Add(Context);
TrackedInstanceIds.Add(_instanceId);
return ValueTask.FromResult(_instanceId);
}
public sealed record TestServiceRetrievalRequest : IRequest<string>;
public static class TestServiceRetrievalHandler
{
public static object? LastRetrievedService { get; set; }
}
public class TestService
{
public string Id { get; } = Guid.NewGuid().ToString();
}
public sealed record TestNestedRequest : IRequest<string>
{
public int Depth { get; init; }
}
public static class TestNestedRequestHandler2
{
public static int ExecutionCount { get; set; }
}
// 生命周期相关类
public sealed record TestLifecycleRequest : IRequest<string>;
public static class TestLifecycleHandler
{
public static int InitializationCount { get; set; }
public static int DisposalCount { get; set; }
}
public sealed record TestScopedServiceRequest : IRequest<int>
{
public int RequestId { get; init; }
}
// 错误处理相关类
public sealed record TestErrorPropagationRequest : IRequest<string>;
public static class TestExceptionHandler
{
public static Exception? LastException { get; set; }
}
public sealed record TestExceptionRequest : IRequest<string>;
// 性能测试相关类
public sealed record TestPerformanceRequest2 : IRequest<int>
{
public int Id { get; init; }
}
public sealed record TestUncachedRequest : IRequest<int>
{
public int Id { get; init; }
}
public sealed record TestCachedRequest : IRequest<int>
{
public int Id { get; init; }
}
// 并发测试相关类
public class SharedState
{
public int Counter { get; set; }
}
public sealed record TestConcurrentRequest : IRequest<int>
{
public int RequestId { get; init; }
public List<int> OrderTracker { get; init; } = new();
}
public sealed record TestStateModificationRequest : IRequest<string>
{
public SharedState SharedState { get; init; } = null!;
public int Increment { get; init; }
}
// 集成测试相关类
public static class TestIntegrationHandler
{
public static string? LastSystemCall { get; set; }
}
public sealed record TestIntegrationRequest : IRequest<string>;
public sealed record TestMediatorRequest : IRequest<int>
{
public int Value { get; init; }
}
/// <summary>
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
/// </summary>
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
// 传统命令用于混合测试
public class TestTraditionalCommand : ICommand
{
public bool Executed { get; private set; }
public void Execute() => Executed = true;
public void SetContext(IArchitectureContext context)
{
/// <summary>
/// 重置跨测试共享的实例跟踪状态。
/// </summary>
public static void Reset()
{
TrackedContexts.Clear();
TrackedInstanceIds.Clear();
_nextInstanceId = 0;
}
}
public IArchitectureContext GetContext() => null!;
}
public sealed record TestContextAwareRequest : IRequest<string>;
#endregion
public static class TestContextAwareHandler
{
public static IArchitectureContext? LastContext { get; set; }
}
public sealed record TestServiceRetrievalRequest : IRequest<string>;
public static class TestServiceRetrievalHandler
{
public static object? LastRetrievedService { get; set; }
}
public class TestService
{
public string Id { get; } = Guid.NewGuid().ToString();
}
public sealed record TestNestedRequest : IRequest<string>
{
public int Depth { get; init; }
}
public static class TestNestedRequestHandler2
{
public static int ExecutionCount { get; set; }
}
// 生命周期相关类
public sealed record TestLifecycleRequest : IRequest<string>;
public static class TestLifecycleHandler
{
public static int InitializationCount { get; set; }
public static int DisposalCount { get; set; }
}
public sealed record TestScopedServiceRequest : IRequest<int>
{
public int RequestId { get; init; }
}
// 错误处理相关类
public sealed record TestErrorPropagationRequest : IRequest<string>;
public static class TestExceptionHandler
{
public static Exception? LastException { get; set; }
}
public sealed record TestExceptionRequest : IRequest<string>;
// 性能测试相关类
public sealed record TestPerformanceRequest2 : IRequest<int>
{
public int Id { get; init; }
}
public sealed record TestUncachedRequest : IRequest<int>
{
public int Id { get; init; }
}
public sealed record TestCachedRequest : IRequest<int>
{
public int Id { get; init; }
}
// 并发测试相关类
public class SharedState
{
private int _counter;
public int Counter => _counter;
public void IncrementBy(int increment)
{
Interlocked.Add(ref _counter, increment);
}
}
public sealed record TestConcurrentRequest : IRequest<int>
{
public int RequestId { get; init; }
public ICollection<int> OrderTracker { get; init; } = new List<int>();
}
public sealed record TestStateModificationRequest : IRequest<string>
{
public SharedState SharedState { get; init; } = null!;
public int Increment { get; init; }
}
// 集成测试相关类
public static class TestIntegrationHandler
{
public static string? LastSystemCall { get; set; }
}
public sealed record TestIntegrationRequest : IRequest<string>;
public sealed record TestMediatorRequest : IRequest<int>
{
public int Value { get; init; }
}
/// <summary>
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
/// </summary>
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
// 传统命令用于混合测试
public class TestTraditionalCommand : ICommand
{
public bool Executed { get; private set; }
public void Execute() => Executed = true;
public void SetContext(IArchitectureContext context)
{
}
public IArchitectureContext GetContext() => null!;
}
#endregion
}

View File

@ -86,7 +86,7 @@ public class MediatorComprehensiveTests
public async Task SendRequestAsync_Should_ReturnResult_When_Request_IsValid()
{
var testRequest = new TestRequest { Value = 42 };
var result = await _context!.SendRequestAsync(testRequest);
var result = await _context!.SendRequestAsync(testRequest).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(42));
}
@ -98,7 +98,7 @@ public class MediatorComprehensiveTests
public void SendRequestAsync_Should_ThrowArgumentNullException_When_Request_IsNull()
{
Assert.ThrowsAsync<ArgumentNullException>(async () =>
await _context!.SendRequestAsync<int>(null!));
await _context!.SendRequestAsync<int>(null!).ConfigureAwait(false));
}
/// <summary>
@ -122,8 +122,8 @@ public class MediatorComprehensiveTests
TestNotificationHandler.LastReceivedMessage = null;
var notification = new TestNotification { Message = "test" };
await _context!.PublishAsync(notification);
await Task.Delay(100);
await _context!.PublishAsync(notification).ConfigureAwait(false);
await Task.Delay(100).ConfigureAwait(false);
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test"));
}
@ -138,7 +138,7 @@ public class MediatorComprehensiveTests
var stream = _context!.CreateStream(testStreamRequest);
var results = new List<int>();
await foreach (var item in stream)
await foreach (var item in stream.ConfigureAwait(false))
{
results.Add(item);
}
@ -153,7 +153,7 @@ public class MediatorComprehensiveTests
public async Task SendAsync_CommandWithoutResult_Should_Execute_When_Command_IsValid()
{
var testCommand = new TestCommand { ShouldExecute = true };
await _context!.SendAsync(testCommand);
await _context!.SendAsync(testCommand).ConfigureAwait(false);
Assert.That(testCommand.Executed, Is.True);
}
@ -165,7 +165,7 @@ public class MediatorComprehensiveTests
public async Task SendAsync_CommandWithResult_Should_ReturnResult_When_Command_IsValid()
{
var testCommand = new TestCommandWithResult { ResultValue = 42 };
var result = await _context!.SendAsync(testCommand);
var result = await _context!.SendAsync(testCommand).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(42));
}
@ -198,7 +198,7 @@ public class MediatorComprehensiveTests
var testRequest = new TestRequest { Value = 42 };
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await contextWithoutHandlers.SendRequestAsync(testRequest));
await contextWithoutHandlers.SendRequestAsync(testRequest).ConfigureAwait(false));
}
/// <summary>
@ -213,8 +213,8 @@ public class MediatorComprehensiveTests
TestNotificationHandler3.LastReceivedMessage = null;
var notification = new TestNotification { Message = "multi-handler test" };
await _context!.PublishAsync(notification);
await Task.Delay(100);
await _context!.PublishAsync(notification).ConfigureAwait(false);
await Task.Delay(100).ConfigureAwait(false);
// 验证所有处理器都被调用
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("multi-handler test"));
@ -233,7 +233,7 @@ public class MediatorComprehensiveTests
// 应该在50ms后被取消
Assert.ThrowsAsync<TaskCanceledException>(async () =>
await _context!.SendRequestAsync(longRequest, cts.Token));
await _context!.SendRequestAsync(longRequest, cts.Token).ConfigureAwait(false));
}
/// <summary>
@ -251,7 +251,7 @@ public class MediatorComprehensiveTests
// 流应该在100ms后被取消TaskCanceledException 继承自 OperationCanceledException
Assert.CatchAsync<OperationCanceledException>(async () =>
{
await foreach (var item in stream)
await foreach (var item in stream.ConfigureAwait(false))
{
results.Add(item);
}
@ -277,7 +277,7 @@ public class MediatorComprehensiveTests
tasks.Add(_context!.SendRequestAsync(request).AsTask());
}
var results = await Task.WhenAll(tasks);
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
// 验证所有结果都正确返回
Assert.That(results.Length, Is.EqualTo(requestCount));
@ -293,7 +293,7 @@ public class MediatorComprehensiveTests
var faultyRequest = new TestFaultyRequest();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _context!.SendRequestAsync(faultyRequest));
await _context!.SendRequestAsync(faultyRequest).ConfigureAwait(false));
}
/// <summary>
@ -306,8 +306,8 @@ public class MediatorComprehensiveTests
var command1 = new TestModifyDataCommand { Data = sharedData, Value = 10 };
var command2 = new TestModifyDataCommand { Data = sharedData, Value = 20 };
await _context!.SendAsync(command1);
await _context.SendAsync(command2);
await _context!.SendAsync(command1).ConfigureAwait(false);
await _context.SendAsync(command2).ConfigureAwait(false);
// 验证数据被正确修改
Assert.That(sharedData.Value, Is.EqualTo(30)); // 10 + 20
@ -331,10 +331,10 @@ public class MediatorComprehensiveTests
foreach (var notification in notifications)
{
await _context!.PublishAsync(notification);
await _context!.PublishAsync(notification).ConfigureAwait(false);
}
await Task.Delay(200); // 等待所有处理完成
await Task.Delay(200).ConfigureAwait(false); // 等待所有处理完成
// 验证接收顺序与发送顺序一致
Assert.That(receivedOrder.Count, Is.EqualTo(3));
@ -358,7 +358,7 @@ public class MediatorComprehensiveTests
var stream = _context!.CreateStream(filterRequest);
var results = new List<int>();
await foreach (var item in stream)
await foreach (var item in stream.ConfigureAwait(false))
{
results.Add(item);
}
@ -377,7 +377,7 @@ public class MediatorComprehensiveTests
var invalidCommand = new TestValidatedCommand { Name = "" }; // 无效:空字符串
Assert.ThrowsAsync<ArgumentException>(async () =>
await _context!.SendAsync(invalidCommand));
await _context!.SendAsync(invalidCommand).ConfigureAwait(false));
}
/// <summary>
@ -392,7 +392,7 @@ public class MediatorComprehensiveTests
for (int i = 0; i < iterations; i++)
{
var request = new TestRequest { Value = i };
var result = await _context!.SendRequestAsync(request);
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(i));
}
@ -417,15 +417,13 @@ public class MediatorComprehensiveTests
// 使用自有 CQRS 方式
var mediatorCommand = new TestCommandWithResult { ResultValue = 999 };
var result = await _context.SendAsync(mediatorCommand);
var result = await _context.SendAsync(mediatorCommand).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(999));
// 验证两者可以同时工作
Assert.That(legacyCommand.Executed, Is.True);
Assert.That(result, Is.EqualTo(999));
}
}
#region Advanced Test Classes for CQRS Features
public sealed record TestLongRunningRequest : IRequest<string>
@ -437,7 +435,7 @@ public sealed class TestLongRunningRequestHandler : IRequestHandler<TestLongRunn
{
public async ValueTask<string> Handle(TestLongRunningRequest request, CancellationToken cancellationToken)
{
await Task.Delay(request.DelayMs, cancellationToken);
await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
return "Completed";
}
@ -458,7 +456,7 @@ public sealed class TestLongStreamRequestHandler : IStreamRequestHandler<TestLon
{
cancellationToken.ThrowIfCancellationRequested();
yield return i;
await Task.Delay(10, cancellationToken); // 模拟处理延迟
await Task.Delay(10, cancellationToken).ConfigureAwait(false); // 模拟处理延迟
}
}
}
@ -496,7 +494,7 @@ public sealed class TestModifyDataCommandHandler : IRequestHandler<TestModifyDat
public sealed record TestCachingQuery : IRequest<string>
{
public string Key { get; init; } = string.Empty;
public Dictionary<string, string> Cache { get; init; } = new();
public IDictionary<string, string> Cache { get; init; } = new Dictionary<string, string>(StringComparer.Ordinal);
}
public sealed class TestCachingQueryHandler : IRequestHandler<TestCachingQuery, string>
@ -522,7 +520,7 @@ public sealed record TestOrderedNotification : INotification
public sealed class TestOrderedNotificationHandler : INotificationHandler<TestOrderedNotification>
{
public static List<string> ReceivedMessages { get; set; } = new();
public static ICollection<string> ReceivedMessages { get; set; } = new List<string>();
public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken)
{
@ -590,7 +588,7 @@ public sealed class TestValidatedCommandHandler : IRequestHandler<TestValidatedC
{
if (string.IsNullOrWhiteSpace(request.Name))
{
throw new ArgumentException($"Name cannot be empty {nameof(request.Name)}");
throw new ArgumentException("Name cannot be empty.", nameof(request));
}
return ValueTask.FromResult(Unit.Value);
@ -719,3 +717,5 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler<TestStreamR
}
#endregion
}

View File

@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证内部 schema 运行时模型会在构造阶段拒绝无效状态,
/// 避免调用方把不一致的约束对象继续传入加载器和校验器。
/// </summary>
[TestFixture]
public sealed class YamlConfigModelContractTests
{
/// <summary>
/// 验证枚举允许值模型会拒绝空白比较键。
/// </summary>
[Test]
public void AllowedValue_Should_Reject_Whitespace_Comparable_Value()
{
Assert.Throws<ArgumentException>(() => new YamlConfigAllowedValue(" ", "visible"));
}
/// <summary>
/// 验证枚举允许值模型会保留空对象等合法结构产生的空比较键。
/// </summary>
[Test]
public void AllowedValue_Should_Accept_Empty_Comparable_Value()
{
var allowedValue = new YamlConfigAllowedValue(string.Empty, "{}");
Assert.That(allowedValue.ComparableValue, Is.Empty);
}
/// <summary>
/// 验证常量约束模型会拒绝空白比较键。
/// </summary>
[Test]
public void ConstantValue_Should_Reject_Whitespace_Comparable_Value()
{
Assert.Throws<ArgumentException>(() => new YamlConfigConstantValue(" ", "\"visible\""));
}
/// <summary>
/// 验证常量约束模型会保留空对象等合法结构产生的空比较键。
/// </summary>
[Test]
public void ConstantValue_Should_Accept_Empty_Comparable_Value()
{
var constantValue = new YamlConfigConstantValue(string.Empty, "{}");
Assert.That(constantValue.ComparableValue, Is.Empty);
}
/// <summary>
/// 验证 contains 约束模型会在构造阶段拦截负值和反向区间。
/// </summary>
[Test]
public void ArrayContainsConstraints_Should_Reject_Invalid_Bounds()
{
var itemNode = CreateStringNode();
Assert.Multiple(() =>
{
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, -1, null));
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, null, -1));
Assert.Throws<ArgumentException>(() => new YamlConfigArrayContainsConstraints(itemNode, 3, 2));
});
}
/// <summary>
/// 验证数组约束模型会在构造阶段拦截负值和反向区间。
/// </summary>
[Test]
public void ArrayConstraints_Should_Reject_Invalid_Bounds()
{
Assert.Multiple(() =>
{
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(-1, null, false, null));
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(null, -1, false, null));
Assert.Throws<ArgumentException>(() => new YamlConfigArrayConstraints(4, 3, false, null));
});
}
/// <summary>
/// 验证对象约束模型会在构造阶段拦截负值和反向区间。
/// </summary>
[Test]
public void ObjectConstraints_Should_Reject_Invalid_Bounds()
{
Assert.Multiple(() =>
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
new YamlConfigObjectConstraints(-1, null, null, null, null, null));
Assert.Throws<ArgumentOutOfRangeException>(() =>
new YamlConfigObjectConstraints(null, -1, null, null, null, null));
Assert.Throws<ArgumentException>(() =>
new YamlConfigObjectConstraints(5, 4, null, null, null, null));
});
}
/// <summary>
/// 验证字符串约束模型要求正则原文与预编译正则成对出现。
/// </summary>
[Test]
public void StringConstraints_Should_Require_Pattern_And_Regex_To_Be_Paired()
{
Assert.Multiple(() =>
{
Assert.Throws<ArgumentException>(() =>
new YamlConfigStringConstraints(null, null, "value", null, null));
Assert.Throws<ArgumentException>(() =>
new YamlConfigStringConstraints(
null,
null,
null,
new Regex("value", RegexOptions.None, TimeSpan.FromSeconds(1)),
null));
});
}
/// <summary>
/// 验证 schema 模型会复制引用表集合,避免外部可变集合继续污染内部状态。
/// </summary>
[Test]
public void Schema_Should_Copy_Referenced_Table_Names()
{
var referencedTableNames = new List<string> { "item" };
var schema = new YamlConfigSchema("monster.schema.json", CreateStringNode(), referencedTableNames);
referencedTableNames.Add("weapon");
Assert.Multiple(() =>
{
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "item" }));
Assert.That(schema.ReferencedTableNames, Is.Not.SameAs(referencedTableNames));
});
}
private static YamlConfigSchemaNode CreateStringNode()
{
return YamlConfigSchemaNode.CreateScalar(
YamlConfigSchemaPropertyType.String,
referenceTableName: null,
allowedValues: null,
constraints: null,
schemaPathHint: "tests.schema.json");
}
}

View File

@ -0,0 +1,39 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个节点上声明的单个 <c>enum</c> 候选值。
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
/// </summary>
internal sealed class YamlConfigAllowedValue
{
/// <summary>
/// 初始化一个枚举候选值模型。
/// </summary>
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
/// <param name="displayValue">用于诊断输出的原始 JSON 文本。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
public YamlConfigAllowedValue(string comparableValue, string displayValue)
{
ArgumentNullException.ThrowIfNull(comparableValue);
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
if (comparableValue.Length > 0 &&
string.IsNullOrWhiteSpace(comparableValue))
{
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
}
ComparableValue = comparableValue;
DisplayValue = displayValue;
}
/// <summary>
/// 获取用于运行时比较的稳定键。
/// </summary>
public string ComparableValue { get; }
/// <summary>
/// 获取用于诊断输出的原始 JSON 文本。
/// </summary>
public string DisplayValue { get; }
}

View File

@ -0,0 +1,66 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。
/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。
/// </summary>
internal sealed class YamlConfigArrayConstraints
{
/// <summary>
/// 初始化数组约束模型。
/// </summary>
/// <param name="minItems">最小元素数量约束。</param>
/// <param name="maxItems">最大元素数量约束。</param>
/// <param name="uniqueItems">是否要求数组元素唯一。</param>
/// <param name="containsConstraints">数组 contains 约束;未声明时为空。</param>
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minItems"/> 或 <paramref name="maxItems"/> 为负数时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="minItems"/> 大于 <paramref name="maxItems"/> 时抛出。</exception>
public YamlConfigArrayConstraints(
int? minItems,
int? maxItems,
bool uniqueItems,
YamlConfigArrayContainsConstraints? containsConstraints)
{
if (minItems is < 0)
{
throw new ArgumentOutOfRangeException(nameof(minItems), minItems, "minItems 不能为负数。");
}
if (maxItems is < 0)
{
throw new ArgumentOutOfRangeException(nameof(maxItems), maxItems, "maxItems 不能为负数。");
}
if (minItems.HasValue &&
maxItems.HasValue &&
minItems.Value > maxItems.Value)
{
throw new ArgumentException("minItems 不能大于 maxItems。", nameof(minItems));
}
MinItems = minItems;
MaxItems = maxItems;
UniqueItems = uniqueItems;
ContainsConstraints = containsConstraints;
}
/// <summary>
/// 获取最小元素数量约束。
/// </summary>
public int? MinItems { get; }
/// <summary>
/// 获取最大元素数量约束。
/// </summary>
public int? MaxItems { get; }
/// <summary>
/// 获取是否要求数组元素唯一。
/// </summary>
public bool UniqueItems { get; }
/// <summary>
/// 获取数组 contains 约束;未声明时返回空。
/// </summary>
public YamlConfigArrayContainsConstraints? ContainsConstraints { get; }
}

View File

@ -0,0 +1,60 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示数组节点声明的 <c>contains</c> 匹配约束。
/// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。
/// </summary>
internal sealed class YamlConfigArrayContainsConstraints
{
/// <summary>
/// 初始化数组 contains 约束模型。
/// </summary>
/// <param name="containsNode">contains 子 schema。</param>
/// <param name="minContains">最小匹配数量;为 <see langword="null" /> 时按 JSON Schema 语义默认 1。</param>
/// <param name="maxContains">最大匹配数量。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="containsNode"/> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minContains"/> 或 <paramref name="maxContains"/> 为负数时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="minContains"/> 大于 <paramref name="maxContains"/> 时抛出。</exception>
public YamlConfigArrayContainsConstraints(
YamlConfigSchemaNode containsNode,
int? minContains,
int? maxContains)
{
ArgumentNullException.ThrowIfNull(containsNode);
if (minContains is < 0)
{
throw new ArgumentOutOfRangeException(nameof(minContains), minContains, "minContains 不能为负数。");
}
if (maxContains is < 0)
{
throw new ArgumentOutOfRangeException(nameof(maxContains), maxContains, "maxContains 不能为负数。");
}
if (minContains.HasValue &&
maxContains.HasValue &&
minContains.Value > maxContains.Value)
{
throw new ArgumentException("minContains 不能大于 maxContains。", nameof(minContains));
}
ContainsNode = containsNode;
MinContains = minContains;
MaxContains = maxContains;
}
/// <summary>
/// 获取 contains 子 schema。
/// </summary>
public YamlConfigSchemaNode ContainsNode { get; }
/// <summary>
/// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。
/// </summary>
public int? MinContains { get; }
/// <summary>
/// 获取最大匹配数量。
/// </summary>
public int? MaxContains { get; }
}

View File

@ -0,0 +1,42 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个对象节点上声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
/// 三个分支都共享父对象已声明字段集合,不会把分支 schema 扩展成新的生成类型形状。
/// </summary>
internal sealed class YamlConfigConditionalSchemas
{
/// <summary>
/// 初始化条件分支约束模型。
/// </summary>
/// <param name="ifSchema">条件判断 schema。</param>
/// <param name="thenSchema">条件命中时需要满足的 schema。</param>
/// <param name="elseSchema">条件未命中时需要满足的 schema。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="ifSchema"/> 为 <see langword="null" /> 时抛出。</exception>
public YamlConfigConditionalSchemas(
YamlConfigSchemaNode ifSchema,
YamlConfigSchemaNode? thenSchema,
YamlConfigSchemaNode? elseSchema)
{
ArgumentNullException.ThrowIfNull(ifSchema);
IfSchema = ifSchema;
ThenSchema = thenSchema;
ElseSchema = elseSchema;
}
/// <summary>
/// 获取条件判断 schema。
/// </summary>
public YamlConfigSchemaNode IfSchema { get; }
/// <summary>
/// 获取条件命中时需要满足的 schema。
/// </summary>
public YamlConfigSchemaNode? ThenSchema { get; }
/// <summary>
/// 获取条件未命中时需要满足的 schema。
/// </summary>
public YamlConfigSchemaNode? ElseSchema { get; }
}

View File

@ -0,0 +1,39 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个节点上声明的 <c>const</c> 约束。
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
/// </summary>
internal sealed class YamlConfigConstantValue
{
/// <summary>
/// 初始化常量约束模型。
/// </summary>
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
/// <param name="displayValue">用于诊断输出的原始常量文本。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
public YamlConfigConstantValue(string comparableValue, string displayValue)
{
ArgumentNullException.ThrowIfNull(comparableValue);
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
if (comparableValue.Length > 0 &&
string.IsNullOrWhiteSpace(comparableValue))
{
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
}
ComparableValue = comparableValue;
DisplayValue = displayValue;
}
/// <summary>
/// 获取用于运行时比较的稳定键。
/// </summary>
public string ComparableValue { get; }
/// <summary>
/// 获取用于诊断输出的原始 JSON 常量文本。
/// </summary>
public string DisplayValue { get; }
}

View File

@ -0,0 +1,55 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示标量节点上声明的数值范围与步进约束。
/// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。
/// </summary>
internal sealed class YamlConfigNumericConstraints
{
/// <summary>
/// 初始化数值约束模型。
/// </summary>
/// <param name="minimum">最小值约束。</param>
/// <param name="maximum">最大值约束。</param>
/// <param name="exclusiveMinimum">开区间最小值约束。</param>
/// <param name="exclusiveMaximum">开区间最大值约束。</param>
/// <param name="multipleOf">数值步进约束。</param>
public YamlConfigNumericConstraints(
double? minimum,
double? maximum,
double? exclusiveMinimum,
double? exclusiveMaximum,
double? multipleOf)
{
Minimum = minimum;
Maximum = maximum;
ExclusiveMinimum = exclusiveMinimum;
ExclusiveMaximum = exclusiveMaximum;
MultipleOf = multipleOf;
}
/// <summary>
/// 获取最小值约束。
/// </summary>
public double? Minimum { get; }
/// <summary>
/// 获取最大值约束。
/// </summary>
public double? Maximum { get; }
/// <summary>
/// 获取开区间最小值约束。
/// </summary>
public double? ExclusiveMinimum { get; }
/// <summary>
/// 获取开区间最大值约束。
/// </summary>
public double? ExclusiveMaximum { get; }
/// <summary>
/// 获取数值步进约束。
/// </summary>
public double? MultipleOf { get; }
}

View File

@ -0,0 +1,86 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个对象节点上声明的属性数量约束、字段依赖约束、条件子 schema 与组合约束。
/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。
/// </summary>
internal sealed class YamlConfigObjectConstraints
{
/// <summary>
/// 初始化对象约束模型。
/// </summary>
/// <param name="minProperties">最小属性数量约束。</param>
/// <param name="maxProperties">最大属性数量约束。</param>
/// <param name="dependentRequired">对象内字段依赖约束。</param>
/// <param name="dependentSchemas">对象内条件 schema 约束。</param>
/// <param name="allOfSchemas">对象内组合 schema 约束。</param>
/// <param name="conditionalSchemas">对象内条件分支约束。</param>
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minProperties"/> 或 <paramref name="maxProperties"/> 为负数时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="minProperties"/> 大于 <paramref name="maxProperties"/> 时抛出。</exception>
public YamlConfigObjectConstraints(
int? minProperties,
int? maxProperties,
IReadOnlyDictionary<string, IReadOnlyList<string>>? dependentRequired,
IReadOnlyDictionary<string, YamlConfigSchemaNode>? dependentSchemas,
IReadOnlyList<YamlConfigSchemaNode>? allOfSchemas,
YamlConfigConditionalSchemas? conditionalSchemas)
{
if (minProperties is < 0)
{
throw new ArgumentOutOfRangeException(nameof(minProperties), minProperties, "minProperties 不能为负数。");
}
if (maxProperties is < 0)
{
throw new ArgumentOutOfRangeException(nameof(maxProperties), maxProperties, "maxProperties 不能为负数。");
}
if (minProperties.HasValue &&
maxProperties.HasValue &&
minProperties.Value > maxProperties.Value)
{
throw new ArgumentException("minProperties 不能大于 maxProperties。", nameof(minProperties));
}
MinProperties = minProperties;
MaxProperties = maxProperties;
DependentRequired = dependentRequired;
DependentSchemas = dependentSchemas;
AllOfSchemas = allOfSchemas;
ConditionalSchemas = conditionalSchemas;
}
/// <summary>
/// 获取最小属性数量约束。
/// </summary>
public int? MinProperties { get; }
/// <summary>
/// 获取最大属性数量约束。
/// </summary>
public int? MaxProperties { get; }
/// <summary>
/// 获取对象内字段依赖约束。
/// 键表示“触发字段”,值表示“触发字段出现后还必须存在的同级字段集合”。
/// </summary>
public IReadOnlyDictionary<string, IReadOnlyList<string>>? DependentRequired { get; }
/// <summary>
/// 获取对象内条件 schema 约束。
/// 键表示“触发字段”,值表示“触发字段出现后当前对象还必须满足的额外 schema 子树”。
/// </summary>
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? DependentSchemas { get; }
/// <summary>
/// 获取对象内 <c>allOf</c> 组合约束。
/// 每个条目都表示“当前对象还必须额外满足的 focused constraint block”。
/// </summary>
public IReadOnlyList<YamlConfigSchemaNode>? AllOfSchemas { get; }
/// <summary>
/// 获取对象内 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
/// 该模型会先用 <c>if</c> 试匹配当前对象,再只对命中的分支叠加 focused constraint block。
/// </summary>
public YamlConfigConditionalSchemas? ConditionalSchemas { get; }
}

View File

@ -0,0 +1,74 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示单个 YAML 文件中提取出的跨表引用。
/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。
/// </summary>
internal sealed class YamlConfigReferenceUsage
{
/// <summary>
/// 初始化一个跨表引用使用记录。
/// </summary>
/// <param name="yamlPath">源 YAML 文件路径。</param>
/// <param name="schemaPath">定义该引用的 schema 文件路径。</param>
/// <param name="propertyPath">声明引用的字段路径。</param>
/// <param name="rawValue">YAML 中的原始标量值。</param>
/// <param name="referencedTableName">目标配置表名称。</param>
/// <param name="valueType">引用值的 schema 标量类型。</param>
public YamlConfigReferenceUsage(
string yamlPath,
string schemaPath,
string propertyPath,
string rawValue,
string referencedTableName,
YamlConfigSchemaPropertyType valueType)
{
ArgumentNullException.ThrowIfNull(yamlPath);
ArgumentNullException.ThrowIfNull(schemaPath);
ArgumentNullException.ThrowIfNull(propertyPath);
ArgumentNullException.ThrowIfNull(rawValue);
ArgumentNullException.ThrowIfNull(referencedTableName);
YamlPath = yamlPath;
SchemaPath = schemaPath;
PropertyPath = propertyPath;
RawValue = rawValue;
ReferencedTableName = referencedTableName;
ValueType = valueType;
}
/// <summary>
/// 获取源 YAML 文件路径。
/// </summary>
public string YamlPath { get; }
/// <summary>
/// 获取定义该引用的 schema 文件路径。
/// </summary>
public string SchemaPath { get; }
/// <summary>
/// 获取声明引用的字段路径。
/// </summary>
public string PropertyPath { get; }
/// <summary>
/// 获取 YAML 中的原始标量值。
/// </summary>
public string RawValue { get; }
/// <summary>
/// 获取目标配置表名称。
/// </summary>
public string ReferencedTableName { get; }
/// <summary>
/// 获取引用值的 schema 标量类型。
/// </summary>
public YamlConfigSchemaPropertyType ValueType { get; }
/// <summary>
/// 获取便于诊断显示的字段路径。
/// </summary>
public string DisplayPath => PropertyPath;
}

View File

@ -0,0 +1,31 @@
namespace GFramework.Game.Config;
/// <summary>
/// 聚合一个标量节点上声明的数值约束与字符串约束。
/// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。
/// </summary>
internal sealed class YamlConfigScalarConstraints
{
/// <summary>
/// 初始化标量约束模型。
/// </summary>
/// <param name="numericConstraints">数值约束分组。</param>
/// <param name="stringConstraints">字符串约束分组。</param>
public YamlConfigScalarConstraints(
YamlConfigNumericConstraints? numericConstraints,
YamlConfigStringConstraints? stringConstraints)
{
NumericConstraints = numericConstraints;
StringConstraints = stringConstraints;
}
/// <summary>
/// 获取数值约束分组。
/// </summary>
public YamlConfigNumericConstraints? NumericConstraints { get; }
/// <summary>
/// 获取字符串约束分组。
/// </summary>
public YamlConfigStringConstraints? StringConstraints { get; }
}

View File

@ -0,0 +1,45 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示已解析并可用于运行时校验的 JSON Schema。
/// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。
/// </summary>
internal sealed class YamlConfigSchema
{
/// <summary>
/// 初始化一个可用于运行时校验的 schema 模型。
/// </summary>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="rootNode">根节点模型。</param>
/// <param name="referencedTableNames">Schema 声明的目标引用表名称集合。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="schemaPath"/>、<paramref name="rootNode"/> 或 <paramref name="referencedTableNames"/> 为 <see langword="null" /> 时抛出。</exception>
public YamlConfigSchema(
string schemaPath,
YamlConfigSchemaNode rootNode,
IReadOnlyCollection<string> referencedTableNames)
{
ArgumentNullException.ThrowIfNull(schemaPath);
ArgumentNullException.ThrowIfNull(rootNode);
ArgumentNullException.ThrowIfNull(referencedTableNames);
SchemaPath = schemaPath;
RootNode = rootNode;
ReferencedTableNames = [.. referencedTableNames];
}
/// <summary>
/// 获取 schema 文件路径。
/// </summary>
public string SchemaPath { get; }
/// <summary>
/// 获取根节点模型。
/// </summary>
public YamlConfigSchemaNode RootNode { get; }
/// <summary>
/// 获取 schema 声明的目标引用表名称集合。
/// 该信息用于热重载时推导受影响的依赖表闭包。
/// </summary>
public IReadOnlyCollection<string> ReferencedTableNames { get; }
}

View File

@ -0,0 +1,321 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示单个 schema 节点的最小运行时描述。
/// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。
/// </summary>
internal sealed class YamlConfigSchemaNode
{
private readonly NodeChildren _children;
private readonly NodeValidation _validation;
private YamlConfigSchemaNode(
YamlConfigSchemaPropertyType nodeType,
NodeChildren children,
NodeValidation validation,
string schemaPathHint)
{
ArgumentNullException.ThrowIfNull(children);
ArgumentNullException.ThrowIfNull(validation);
ArgumentNullException.ThrowIfNull(schemaPathHint);
_children = children;
_validation = validation;
NodeType = nodeType;
Properties = children.Properties;
RequiredProperties = children.RequiredProperties;
ItemNode = children.ItemNode;
ReferenceTableName = validation.ReferenceTableName;
AllowedValues = validation.AllowedValues;
Constraints = validation.Constraints;
ArrayConstraints = validation.ArrayConstraints;
ObjectConstraints = validation.ObjectConstraints;
ConstantValue = validation.ConstantValue;
NegatedSchemaNode = validation.NegatedSchemaNode;
SchemaPathHint = schemaPathHint;
}
/// <summary>
/// 获取节点类型。
/// </summary>
public YamlConfigSchemaPropertyType NodeType { get; }
/// <summary>
/// 获取对象属性集合;非对象节点时返回空。
/// </summary>
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
/// <summary>
/// 获取对象必填属性集合;非对象节点时返回空。
/// </summary>
public IReadOnlyCollection<string>? RequiredProperties { get; }
/// <summary>
/// 获取数组元素节点;非数组节点时返回空。
/// </summary>
public YamlConfigSchemaNode? ItemNode { get; }
/// <summary>
/// 获取目标引用表名称;未声明跨表引用时返回空。
/// </summary>
public string? ReferenceTableName { get; }
/// <summary>
/// 获取节点允许值集合;未声明 <c>enum</c> 时返回空。
/// </summary>
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
/// <summary>
/// 获取标量范围与长度约束;未声明时返回空。
/// </summary>
public YamlConfigScalarConstraints? Constraints { get; }
/// <summary>
/// 获取对象属性数量约束;未声明时返回空。
/// </summary>
public YamlConfigObjectConstraints? ObjectConstraints { get; }
/// <summary>
/// 获取数组元素数量约束;未声明时返回空。
/// </summary>
public YamlConfigArrayConstraints? ArrayConstraints { get; }
/// <summary>
/// 获取节点常量约束;未声明 <c>const</c> 时返回空。
/// </summary>
public YamlConfigConstantValue? ConstantValue { get; }
/// <summary>
/// 获取节点声明的 <c>not</c> 子 schema未声明时返回空。
/// </summary>
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
/// <summary>
/// 获取用于诊断显示的 schema 路径提示。
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
/// </summary>
public string SchemaPathHint { get; }
/// <summary>
/// 创建对象节点描述。
/// </summary>
/// <param name="properties">对象属性集合。</param>
/// <param name="requiredProperties">对象必填属性集合。</param>
/// <param name="objectConstraints">对象属性数量约束。</param>
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
/// <returns>对象节点模型。</returns>
public static YamlConfigSchemaNode CreateObject(
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
IReadOnlyCollection<string>? requiredProperties,
YamlConfigObjectConstraints? objectConstraints,
string schemaPathHint)
{
return new YamlConfigSchemaNode(
YamlConfigSchemaPropertyType.Object,
new NodeChildren(properties, requiredProperties, itemNode: null),
new NodeValidation(
referenceTableName: null,
allowedValues: null,
constraints: null,
arrayConstraints: null,
objectConstraints,
constantValue: null,
negatedSchemaNode: null),
schemaPathHint);
}
/// <summary>
/// 创建数组节点描述。
/// </summary>
/// <param name="itemNode">数组元素节点。</param>
/// <param name="allowedValues">数组节点允许值集合。</param>
/// <param name="arrayConstraints">数组元素数量约束。</param>
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
/// <returns>数组节点模型。</returns>
public static YamlConfigSchemaNode CreateArray(
YamlConfigSchemaNode itemNode,
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
YamlConfigArrayConstraints? arrayConstraints,
string schemaPathHint)
{
return new YamlConfigSchemaNode(
YamlConfigSchemaPropertyType.Array,
new NodeChildren(properties: null, requiredProperties: null, itemNode),
new NodeValidation(
referenceTableName: null,
allowedValues,
constraints: null,
arrayConstraints,
objectConstraints: null,
constantValue: null,
negatedSchemaNode: null),
schemaPathHint);
}
/// <summary>
/// 创建标量节点描述。
/// </summary>
/// <param name="nodeType">标量节点类型。</param>
/// <param name="referenceTableName">目标引用表名称。</param>
/// <param name="allowedValues">标量允许值集合。</param>
/// <param name="constraints">标量范围与长度约束。</param>
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
/// <returns>标量节点模型。</returns>
public static YamlConfigSchemaNode CreateScalar(
YamlConfigSchemaPropertyType nodeType,
string? referenceTableName,
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
YamlConfigScalarConstraints? constraints,
string schemaPathHint)
{
return new YamlConfigSchemaNode(
nodeType,
NodeChildren.None,
new NodeValidation(
referenceTableName,
allowedValues,
constraints,
arrayConstraints: null,
objectConstraints: null,
constantValue: null,
negatedSchemaNode: null),
schemaPathHint);
}
/// <summary>
/// 基于当前节点复制一个只替换引用表名称的新节点。
/// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。
/// </summary>
/// <param name="referenceTableName">新的目标引用表名称。</param>
/// <returns>复制后的节点。</returns>
public YamlConfigSchemaNode WithReferenceTable(string referenceTableName)
{
return new YamlConfigSchemaNode(
NodeType,
_children,
_validation.WithReferenceTable(referenceTableName),
SchemaPathHint);
}
/// <summary>
/// 基于当前节点复制一个只替换 <c>enum</c> 允许值集合的新节点。
/// </summary>
/// <param name="allowedValues">新的允许值集合。</param>
/// <returns>复制后的节点。</returns>
public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
{
return new YamlConfigSchemaNode(
NodeType,
_children,
_validation.WithAllowedValues(allowedValues),
SchemaPathHint);
}
/// <summary>
/// 基于当前节点复制一个只替换常量约束的新节点。
/// </summary>
/// <param name="constantValue">新的常量约束。</param>
/// <returns>复制后的节点。</returns>
public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue)
{
return new YamlConfigSchemaNode(
NodeType,
_children,
_validation.WithConstantValue(constantValue),
SchemaPathHint);
}
/// <summary>
/// 基于当前节点复制一个只替换 <c>not</c> 子 schema 的新节点。
/// </summary>
/// <param name="negatedSchemaNode">新的 negated schema。</param>
/// <returns>复制后的节点。</returns>
public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
{
return new YamlConfigSchemaNode(
NodeType,
_children,
_validation.WithNegatedSchemaNode(negatedSchemaNode),
SchemaPathHint);
}
private sealed class NodeChildren
{
public NodeChildren(
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
IReadOnlyCollection<string>? requiredProperties,
YamlConfigSchemaNode? itemNode)
{
Properties = properties;
RequiredProperties = requiredProperties;
ItemNode = itemNode;
}
public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null);
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
public IReadOnlyCollection<string>? RequiredProperties { get; }
public YamlConfigSchemaNode? ItemNode { get; }
}
private sealed class NodeValidation
{
public NodeValidation(
string? referenceTableName,
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
YamlConfigScalarConstraints? constraints,
YamlConfigArrayConstraints? arrayConstraints,
YamlConfigObjectConstraints? objectConstraints,
YamlConfigConstantValue? constantValue,
YamlConfigSchemaNode? negatedSchemaNode)
{
ReferenceTableName = referenceTableName;
AllowedValues = allowedValues;
Constraints = constraints;
ArrayConstraints = arrayConstraints;
ObjectConstraints = objectConstraints;
ConstantValue = constantValue;
NegatedSchemaNode = negatedSchemaNode;
}
public string? ReferenceTableName { get; }
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
public YamlConfigScalarConstraints? Constraints { get; }
public YamlConfigArrayConstraints? ArrayConstraints { get; }
public YamlConfigObjectConstraints? ObjectConstraints { get; }
public YamlConfigConstantValue? ConstantValue { get; }
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
public NodeValidation WithReferenceTable(string referenceTableName)
{
return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints,
ObjectConstraints, ConstantValue, NegatedSchemaNode);
}
public NodeValidation WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
{
return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints,
ObjectConstraints, ConstantValue, NegatedSchemaNode);
}
public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue)
{
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
ObjectConstraints, constantValue, NegatedSchemaNode);
}
public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
{
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
ObjectConstraints, ConstantValue, negatedSchemaNode);
}
}
}

View File

@ -0,0 +1,40 @@
using GFramework.Core.SourceGenerators.Abstractions.Enums;
namespace GFramework.Game.Config;
/// <summary>
/// 表示当前运行时 schema 校验器支持的属性类型。
/// </summary>
[GenerateEnumExtensions]
internal enum YamlConfigSchemaPropertyType
{
/// <summary>
/// 对象类型。
/// </summary>
Object,
/// <summary>
/// 整数类型。
/// </summary>
Integer,
/// <summary>
/// 数值类型。
/// </summary>
Number,
/// <summary>
/// 布尔类型。
/// </summary>
Boolean,
/// <summary>
/// 字符串类型。
/// </summary>
String,
/// <summary>
/// 数组类型。
/// </summary>
Array
}

View File

@ -104,67 +104,12 @@ internal static partial class YamlConfigSchemaValidator
var dependentRequired = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
foreach (var dependency in dependentRequiredElement.EnumerateObject())
{
if (!properties.ContainsKey(dependency.Name))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (dependency.Value.ValueKind != JsonValueKind.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencyTargets = new List<string>();
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
{
if (dependencyTarget.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencyTargetName = dependencyTarget.GetString();
if (string.IsNullOrWhiteSpace(dependencyTargetName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (!properties.ContainsKey(dependencyTargetName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (seenDependencyTargets.Add(dependencyTargetName))
{
dependencyTargets.Add(dependencyTargetName);
}
}
var dependencyTargets = ParseDependentRequiredConstraint(
tableName,
schemaPath,
propertyPath,
dependency,
properties);
if (dependencyTargets.Count > 0)
{
dependentRequired[dependency.Name] = dependencyTargets;
@ -176,6 +121,116 @@ internal static partial class YamlConfigSchemaValidator
: dependentRequired;
}
/// <summary>
/// 解析单个 <c>dependentRequired</c> 触发字段的依赖目标列表。
/// 触发字段和目标字段必须都来自父对象已声明属性;重复目标会被去重以保持运行时约束稳定。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">对象字段路径。</param>
/// <param name="dependency">当前触发字段声明。</param>
/// <param name="properties">当前对象已声明的属性集合。</param>
/// <returns>去重后的依赖目标列表。</returns>
private static IReadOnlyList<string> ParseDependentRequiredConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonProperty dependency,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (!properties.ContainsKey(dependency.Name))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (dependency.Value.ValueKind != JsonValueKind.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencyTargets = new List<string>();
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
{
var dependencyTargetName = ParseDependentRequiredTargetName(
tableName,
schemaPath,
propertyPath,
dependency.Name,
dependencyTarget,
properties);
if (seenDependencyTargets.Add(dependencyTargetName))
{
dependencyTargets.Add(dependencyTargetName);
}
}
return dependencyTargets;
}
/// <summary>
/// 读取并校验 <c>dependentRequired</c> 的单个目标字段名。
/// 目标必须是非空字符串并已在同一个对象 schema 中声明,避免依赖关系指向不可满足字段。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">对象字段路径。</param>
/// <param name="dependencyName">当前触发字段名称。</param>
/// <param name="dependencyTarget">当前依赖目标节点。</param>
/// <param name="properties">当前对象已声明的属性集合。</param>
/// <returns>已校验的依赖目标字段名。</returns>
private static string ParseDependentRequiredTargetName(
string tableName,
string schemaPath,
string propertyPath,
string dependencyName,
JsonElement dependencyTarget,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (dependencyTarget.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencyTargetName = dependencyTarget.GetString();
if (string.IsNullOrWhiteSpace(dependencyTargetName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (properties.ContainsKey(dependencyTargetName))
{
return dependencyTargetName;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
/// <summary>
/// 解析对象节点声明的 <c>dependentSchemas</c> 条件 schema。
/// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释
@ -212,43 +267,12 @@ internal static partial class YamlConfigSchemaValidator
var dependentSchemas = new Dictionary<string, YamlConfigSchemaNode>(StringComparer.Ordinal);
foreach (var dependency in dependentSchemasElement.EnumerateObject())
{
if (!properties.ContainsKey(dependency.Name))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (dependency.Value.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
var dependencySchemaNode = ParseNode(
dependentSchemas[dependency.Name] = ParseDependentSchemaConstraint(
tableName,
schemaPath,
dependencySchemaPath,
dependency.Value);
if (dependencySchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(dependencySchemaPath));
}
dependentSchemas[dependency.Name] = dependencySchemaNode;
propertyPath,
dependency,
properties);
}
return dependentSchemas.Count == 0
@ -256,6 +280,62 @@ internal static partial class YamlConfigSchemaValidator
: dependentSchemas;
}
/// <summary>
/// 解析单个 <c>dependentSchemas</c> 触发字段关联的 object-typed schema。
/// 触发字段必须属于当前对象,关联 schema 继续通过通用节点解析流程获得完整约束模型。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">对象字段路径。</param>
/// <param name="dependency">当前触发字段声明。</param>
/// <param name="properties">当前对象已声明的属性集合。</param>
/// <returns>解析后的 object-typed 条件 schema。</returns>
private static YamlConfigSchemaNode ParseDependentSchemaConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonProperty dependency,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (!properties.ContainsKey(dependency.Name))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (dependency.Value.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
var dependencySchemaNode = ParseNode(
tableName,
schemaPath,
dependencySchemaPath,
dependency.Value);
if (dependencySchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
{
return dependencySchemaNode;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(dependencySchemaPath));
}
/// <summary>
/// 解析对象节点声明的 <c>allOf</c> 组合约束。
/// 当前实现仅接受 object-typed 内联 schema并把每个条目当成 focused constraint block
@ -293,40 +373,13 @@ internal static partial class YamlConfigSchemaValidator
var allOfIndex = 0;
foreach (var allOfSchemaElement in allOfElement.EnumerateArray())
{
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
ValidateInlineObjectSchemaTargetsAgainstParentObject(
var allOfSchemaNode = ParseAllOfSchemaConstraint(
tableName,
schemaPath,
propertyPath,
allOfSchemaPath,
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'",
allOfSchemaElement,
allOfIndex,
properties);
var allOfSchemaNode = ParseNode(
tableName,
schemaPath,
allOfSchemaPath,
allOfSchemaElement);
if (allOfSchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(allOfSchemaPath));
}
allOfSchemas.Add(allOfSchemaNode);
allOfIndex++;
}
@ -336,6 +389,64 @@ internal static partial class YamlConfigSchemaValidator
: allOfSchemas;
}
/// <summary>
/// 解析 <c>allOf</c> 中的单个 object-focused schema 条目。
/// 每个条目只允许约束父对象已声明的字段,并且必须保持 object-typed 语义。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">父对象路径。</param>
/// <param name="allOfSchemaElement">当前 allOf 条目。</param>
/// <param name="allOfIndex">当前 allOf 条目的零基索引。</param>
/// <param name="properties">父对象已声明的属性集合。</param>
/// <returns>解析后的 object-typed schema。</returns>
private static YamlConfigSchemaNode ParseAllOfSchemaConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement allOfSchemaElement,
int allOfIndex,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var allOfEntryLabel = $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'";
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
ValidateInlineObjectSchemaTargetsAgainstParentObject(
tableName,
schemaPath,
propertyPath,
allOfSchemaPath,
allOfEntryLabel,
allOfSchemaElement,
properties);
var allOfSchemaNode = ParseNode(
tableName,
schemaPath,
allOfSchemaPath,
allOfSchemaElement);
if (allOfSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
{
return allOfSchemaNode;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{allOfEntryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(allOfSchemaPath));
}
/// <summary>
/// 解析对象节点声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
/// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义,
@ -362,25 +473,7 @@ internal static partial class YamlConfigSchemaValidator
return null;
}
if (!hasIf)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (!hasThen && !hasElse)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
ValidateConditionalSchemaKeywordPresence(tableName, schemaPath, propertyPath, hasIf, hasThen, hasElse);
var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if");
var ifSchemaNode = ParseConditionalObjectSchema(
@ -391,31 +484,100 @@ internal static partial class YamlConfigSchemaValidator
"if",
ifElement,
properties);
var thenSchemaNode = hasThen
? ParseConditionalObjectSchema(
tableName,
schemaPath,
propertyPath,
BuildNestedSchemaPath(propertyPath, "then"),
"then",
thenElement,
properties)
: null;
var elseSchemaNode = hasElse
? ParseConditionalObjectSchema(
tableName,
schemaPath,
propertyPath,
BuildNestedSchemaPath(propertyPath, "else"),
"else",
elseElement,
properties)
: null;
var thenSchemaNode = ParseOptionalConditionalObjectSchema(
tableName,
schemaPath,
propertyPath,
"then",
hasThen,
thenElement,
properties);
var elseSchemaNode = ParseOptionalConditionalObjectSchema(
tableName,
schemaPath,
propertyPath,
"else",
hasElse,
elseElement,
properties);
return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode);
}
/// <summary>
/// 校验 object-focused 条件关键字的组合关系。
/// <c>then</c> 与 <c>else</c> 只能跟随 <c>if</c>,而单独的 <c>if</c> 没有可执行分支。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">对象字段路径。</param>
/// <param name="hasIf">是否声明 if。</param>
/// <param name="hasThen">是否声明 then。</param>
/// <param name="hasElse">是否声明 else。</param>
private static void ValidateConditionalSchemaKeywordPresence(
string tableName,
string schemaPath,
string propertyPath,
bool hasIf,
bool hasThen,
bool hasElse)
{
if (!hasIf)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (hasThen || hasElse)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
/// <summary>
/// 解析可选的 <c>then</c> 或 <c>else</c> 条件分支。
/// 未声明的分支保留为空,声明的分支必须通过 object-focused schema 校验。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">对象字段路径。</param>
/// <param name="keywordName">条件关键字名称。</param>
/// <param name="hasKeyword">是否声明该条件关键字。</param>
/// <param name="keywordElement">条件关键字对应的 schema 节点。</param>
/// <param name="properties">父对象已声明的属性集合。</param>
/// <returns>解析后的条件分支;未声明时返回空。</returns>
private static YamlConfigSchemaNode? ParseOptionalConditionalObjectSchema(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
bool hasKeyword,
JsonElement keywordElement,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
return hasKeyword
? ParseConditionalObjectSchema(
tableName,
schemaPath,
propertyPath,
BuildNestedSchemaPath(propertyPath, keywordName),
keywordName,
keywordElement,
properties)
: null;
}
/// <summary>
/// 解析单个条件分支的 object-focused 内联 schema。
/// </summary>
@ -494,34 +656,96 @@ internal static partial class YamlConfigSchemaValidator
JsonElement inlineSchemaElement,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
ValidateInlineObjectSchemaPropertiesAgainstParentObject(
tableName,
schemaPath,
propertyPath,
inlineSchemaPath,
entryLabel,
inlineSchemaElement,
properties);
ValidateInlineRequiredPropertiesAgainstParentObject(
tableName,
schemaPath,
propertyPath,
inlineSchemaPath,
entryLabel,
inlineSchemaElement,
properties);
}
/// <summary>
/// 校验 object-focused 内联 schema 的 <c>properties</c> 只引用父对象字段。
/// focused block 不负责声明新字段,所以任何父对象未声明字段都会在 schema 加载时被拒绝。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">父对象路径。</param>
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
/// <param name="inlineSchemaElement">当前内联 schema。</param>
/// <param name="properties">父对象已声明的属性集合。</param>
private static void ValidateInlineObjectSchemaPropertiesAgainstParentObject(
string tableName,
string schemaPath,
string propertyPath,
string inlineSchemaPath,
string entryLabel,
JsonElement inlineSchemaElement,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (!inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
{
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
foreach (var property in inlinePropertiesElement.EnumerateObject())
{
if (properties.ContainsKey(property.Name))
{
continue;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
return;
}
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
foreach (var property in inlinePropertiesElement.EnumerateObject())
{
if (properties.ContainsKey(property.Name))
{
continue;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
}
/// <summary>
/// 校验 object-focused 内联 schema 的 <c>required</c> 只引用父对象字段。
/// 该校验在加载期暴露不可满足的条件块,而不是等到运行时才发现无效字段名。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">父对象路径。</param>
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
/// <param name="inlineSchemaElement">当前内联 schema。</param>
/// <param name="properties">父对象已声明的属性集合。</param>
private static void ValidateInlineRequiredPropertiesAgainstParentObject(
string tableName,
string schemaPath,
string propertyPath,
string inlineSchemaPath,
string entryLabel,
JsonElement inlineSchemaElement,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement))
{
return;
@ -539,39 +763,69 @@ internal static partial class YamlConfigSchemaValidator
foreach (var requiredProperty in inlineRequiredElement.EnumerateArray())
{
if (requiredProperty.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
var requiredPropertyName = requiredProperty.GetString();
if (string.IsNullOrWhiteSpace(requiredPropertyName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
if (properties.ContainsKey(requiredPropertyName))
{
continue;
}
ValidateInlineRequiredPropertyAgainstParentObject(
tableName,
schemaPath,
propertyPath,
inlineSchemaPath,
entryLabel,
requiredProperty,
properties);
}
}
/// <summary>
/// 校验 object-focused 内联 schema 的单个 <c>required</c> 字段名。
/// 字段名必须是非空字符串并且属于父对象声明范围,保持条件块与父对象形状一致。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
/// <param name="propertyPath">父对象路径。</param>
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
/// <param name="requiredProperty">当前 required 条目。</param>
/// <param name="properties">父对象已声明的属性集合。</param>
private static void ValidateInlineRequiredPropertyAgainstParentObject(
string tableName,
string schemaPath,
string propertyPath,
string inlineSchemaPath,
string entryLabel,
JsonElement requiredProperty,
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
{
if (requiredProperty.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
var requiredPropertyName = requiredProperty.GetString();
if (string.IsNullOrWhiteSpace(requiredPropertyName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
if (properties.ContainsKey(requiredPropertyName))
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(inlineSchemaPath));
}
/// <summary>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
using System.Text.RegularExpressions;
namespace GFramework.Game.Config;
/// <summary>
/// 表示标量节点上声明的字符串长度、模式与 format 约束。
/// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存,
/// 保证诊断内容与运行时匹配逻辑保持一致。
/// </summary>
internal sealed class YamlConfigStringConstraints
{
/// <summary>
/// 初始化字符串约束模型。
/// </summary>
/// <param name="minLength">最小长度约束。</param>
/// <param name="maxLength">最大长度约束。</param>
/// <param name="pattern">正则模式约束原文。</param>
/// <param name="patternRegex">已编译的正则表达式。</param>
/// <param name="formatConstraint">字符串 format 约束。</param>
/// <exception cref="ArgumentException">当 <paramref name="pattern"/> 与 <paramref name="patternRegex"/> 未成对出现时抛出。</exception>
public YamlConfigStringConstraints(
int? minLength,
int? maxLength,
string? pattern,
Regex? patternRegex,
YamlConfigStringFormatConstraint? formatConstraint)
{
if ((pattern is null) != (patternRegex is null))
{
throw new ArgumentException("pattern 与 patternRegex 必须同时为空或同时提供。", nameof(pattern));
}
MinLength = minLength;
MaxLength = maxLength;
Pattern = pattern;
PatternRegex = patternRegex;
FormatConstraint = formatConstraint;
}
/// <summary>
/// 获取最小长度约束。
/// </summary>
public int? MinLength { get; }
/// <summary>
/// 获取最大长度约束。
/// </summary>
public int? MaxLength { get; }
/// <summary>
/// 获取正则模式约束原文。
/// </summary>
public string? Pattern { get; }
/// <summary>
/// 获取已编译的正则表达式。
/// </summary>
public Regex? PatternRegex { get; }
/// <summary>
/// 获取字符串 format 约束。
/// </summary>
public YamlConfigStringFormatConstraint? FormatConstraint { get; }
}

View File

@ -0,0 +1,35 @@
namespace GFramework.Game.Config;
/// <summary>
/// 表示一个已归一化的字符串 format 约束。
/// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。
/// </summary>
internal sealed class YamlConfigStringFormatConstraint
{
/// <summary>
/// 初始化字符串 format 约束模型。
/// </summary>
/// <param name="schemaName">schema 中声明的 format 名称。</param>
/// <param name="kind">归一化后的共享 format 枚举。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="schemaName"/> 为 <see langword="null" /> 时抛出。</exception>
/// <exception cref="ArgumentException">当 <paramref name="schemaName"/> 为空或仅包含空白字符时抛出。</exception>
public YamlConfigStringFormatConstraint(
string schemaName,
YamlConfigStringFormatKind kind)
{
ArgumentException.ThrowIfNullOrWhiteSpace(schemaName);
SchemaName = schemaName;
Kind = kind;
}
/// <summary>
/// 获取 schema 中声明的 format 名称。
/// </summary>
public string SchemaName { get; }
/// <summary>
/// 获取归一化后的共享 format 枚举。
/// </summary>
public YamlConfigStringFormatKind Kind { get; }
}

View File

@ -0,0 +1,45 @@
using GFramework.Core.SourceGenerators.Abstractions.Enums;
namespace GFramework.Game.Config;
/// <summary>
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
/// </summary>
[GenerateEnumExtensions]
internal enum YamlConfigStringFormatKind
{
/// <summary>
/// 表示 <c>yyyy-MM-dd</c> 形式的日期。
/// </summary>
Date,
/// <summary>
/// 表示带显式时区偏移的 RFC 3339 日期时间。
/// </summary>
DateTime,
/// <summary>
/// 表示 day-time duration 形式的持续时间。
/// </summary>
Duration,
/// <summary>
/// 表示基础电子邮件地址格式。
/// </summary>
Email,
/// <summary>
/// 表示带显式时区偏移的 RFC 3339 时间。
/// </summary>
Time,
/// <summary>
/// 表示绝对 URI。
/// </summary>
Uri,
/// <summary>
/// 表示连字符分隔的 UUID 文本。
/// </summary>
Uuid
}

View File

@ -9,6 +9,7 @@
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Core.SourceGenerators.Abstractions\GFramework.Core.SourceGenerators.Abstractions.csproj" />
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
</ItemGroup>

View File

@ -12,11 +12,6 @@ help the current worktree land on the right recovery documents without scanning
## Active Topics
- `analyzer-warning-reduction`
- Purpose: track the analyzer warning reduction branch, including the current recovery point, remaining warning
hotspots, and the next safe warning-reduction slice.
- Tracking: `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
- Trace: `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
- `ai-plan-governance`
- Purpose: govern the `ai-plan/` directory model, startup index, and archive policy.
- Tracking: `ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md`
@ -50,9 +45,6 @@ help the current worktree land on the right recovery documents without scanning
## Worktree To Active Topic Map
- Branch: `fix/analyzer-warning-reduction-batch`
- Worktree hint: `GFramework-analyzer`
- Priority 1: `analyzer-warning-reduction`
- Branch: `feat/ai-first-config`
- Worktree hint: `GFramework-Ai-First-Config`
- Priority 1: `ai-first-config-system`
@ -75,6 +67,9 @@ help the current worktree land on the right recovery documents without scanning
- Priority 1: `documentation-full-coverage-governance`
## Archived Topics
- `analyzer-warning-reduction`
- Archive root: `ai-plan/public/archive/analyzer-warning-reduction/`
- Note: 长期 warning-reduction 分支已收尾PR #301 的最终 review follow-up 已本地闭环,后续仅作为历史恢复材料保留。
- `cqrs-cache-docs-hardening`
- Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
- Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.

View File

@ -1,64 +0,0 @@
# Analyzer Warning Reduction 跟踪
## 目标
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值。
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-092`
- 当前阶段:`Phase 92`
- 当前焦点:
- `2026-04-28` 复核 `PR #300` 最新 open threads代码类线程已与当前工作树对齐仅剩 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的文件计数与验证口径漂移仍然成立
- 已将 tracking 文档修正为与 `6cc87a9...HEAD` 的实际变更规模一致,并与 trace 中记录的 `dotnet build`、定向 `dotnet test``git diff --check` 验证口径保持一致
- `dotnet format --verify-no-changes``GFramework.Core.Tests` 既有 `FINALNEWLINE``CHARSET``WHITESPACE` 基线仍保持独立,不与当前 `ai-plan` 同步修复混提
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `6cc87a9``2026-04-27T20:28:50+08:00`)。
- 当前直接验证结果:
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
- 最新结果:成功;`67` 通过、`0` 失败
- 当前批次摘要:
- 当前分支相对 `6cc87a9...HEAD` 包含 `18` 个已修改文件与 `38` 个新增文件(合计 `56` 个变更文件),分别位于 `GFramework.Core.Tests``GFramework.Cqrs.Tests``GFramework.Core``.agents/skills/gframework-pr-review/``ai-plan/public/analyzer-warning-reduction`
- 本轮没有触碰 `Mediator/*``YamlConfigSchemaValidator*``GFramework.Core.Tests` 的整项目格式基线波次
## 当前风险
- GitHub PR 上的 open threads 可能仍显示为未关闭,因为当前只同步了 `ai-plan` 文档,尚未推送新的 head 供审查机器人重新折叠线程。
- 缓解措施:推送本次 `ai-plan` 同步提交后重新执行 `$gframework-pr-review`,以最新 head 再核对 thread 状态。
- `dotnet format GFramework.Core.Tests/GFramework.Core.Tests.csproj --verify-no-changes` 当前会命中项目内大量历史格式诊断。
- 缓解措施:本轮只记录为现存基线,不把 `PR #300` 的 review follow-up 扩展成整项目格式清理。
- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集。
- 缓解措施:继续保持为独立高耦合波次。
## 活跃文档
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
## 验证说明
- 权威验证结果统一维护在“当前活跃事实”。
- `GFramework.Core.Tests` 的当前受影响项目 Release 构建已清零,并通过对应定向测试回归。
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test``git diff --check` 为准,并与 trace 中的验证里程碑保持一致。
## 下一步建议
1. 提交本轮 `PR #300` nitpick follow-up、技能规则更新与 `ai-plan` 同步。
2. 推送后重新执行 `$gframework-pr-review`,确认 `ai-plan` 相关 thread 是否随最新 head 自动收口。
3. 若要清理 `dotnet format` 基线,另开 `GFramework.Core.Tests` 格式治理切片,不与当前 PR review 修复混提。

View File

@ -1,67 +0,0 @@
# Analyzer Warning Reduction 追踪
## 2026-04-28 — RP-092
### 阶段:复核 `PR #300` 的 open threads并只修正当前分支仍然成立的 `ai-plan` 漂移
- 触发背景:
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
- 主线程实施:
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests``TestArchitectureWithRegistry``TestResourceLoader``PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op定向回归测试通过
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test``git diff --check`
- 验证里程碑:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
- 结果:成功;`10` 通过、`0` 失败
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
## 2026-04-28 — RP-091
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick并升级 PR-review triage 规则
- 触发背景:
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md``$gframework-pr-review`
- 主线程实施:
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
- 将 `TestArchitectureContext``TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`明确要求latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
- 验证里程碑:
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
- 结果:成功;`67` 通过、`0` 失败
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
## 活跃风险
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
## 下一步
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
## 历史归档指针
- 最新 trace 归档:
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- 历史 todo 归档:
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 早期归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)

View File

@ -0,0 +1,72 @@
# Analyzer Warning Reduction 跟踪
## 目标
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值。
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-096`
- 当前阶段:`Completed`
- 当前焦点:
- `2026-04-29` 已完成 `PR #301` latest-head review threads 的最终本地复核,并修复仍然成立的空对象 `const` 比较键回归
- 当前 topic 已达到归档条件:长期 warning-reduction 分支的实现、PR review follow-up 与最小验证均已完成
- 当前目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,后续仅保留历史恢复价值
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `0e32dab``2026-04-28T17:15:47+08:00`)。
- 当前直接验证结果:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
- 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
- 最新结果:成功;`10` 通过、`0` 失败
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
- 最新结果:成功;`25` 通过、`0` 失败
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
- 最新结果:成功;当前修复范围内无格式漂移
- `git diff --check`
- 最新结果:成功;无新增 whitespace / conflict-marker 问题
- 当前批次摘要:
- 当前最终收尾切片直接修改 `3` 个已有文件,不再扩写 warning-batch 的多文件清理范围
- 这次收尾把 `YamlConfigAllowedValue` / `YamlConfigConstantValue``comparableValue` 契约收窄为“允许空字符串,但拒绝非空纯空白”,恢复空对象 `const` / `enum` 的合法比较键语义
- PR review triage 结论:
- 接受并完成:并发共享状态、阻塞等待、无效约束状态、缺失 `<exception>` 文档、空对象比较键回归
- 归档前剩余 open threads 只包含两类:尚未推送折叠的 stale 线程,以及已明确延后 / 驳回的建议(`DisplayPath` 与枚举特性泛化)
## 当前风险
- 当前 GitHub PR 在本地提交并推送前仍可能显示旧的 open threads。
- 缓解措施:以本文件中的本地验证结果为 archive 真值;若未来需要复查 PR 页面,应从 archive 恢复而不是重新激活 topic。
- 本轮仅对 `GFramework.Game` 收尾回归做了受影响模块验证,没有重新建立新的仓库根 clean build 基线。
- 缓解措施:后续若有新的 warning-reduction 任务,应创建新 topic并重新执行仓库根 `dotnet clean` + `dotnet build` 采样。
## 活跃文档
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
## 验证说明
- 权威验证结果统一维护在“当前活跃事实”。
- `GFramework.Game` 当前 Release 构建已清零,并通过空对象 `const` 回归与模型契约定向测试。
- `GFramework.Cqrs.Tests` 当前 PR-review follow-up 定向测试通过,说明并发/缓存测试辅助实现的行为修正没有破坏现有集成断言。
- `dotnet format --verify-no-changes` 已确认当前收尾改动未引入新的格式化偏差。
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
- 本 topic 已进入 archive若未来重启 warning reduction应以新 topic 和新的仓库级 clean build 基线继续。
## 下一步建议
1. 保持当前 archive 状态,不要再把该 topic 作为默认 boot 入口。
2. 若未来需要继续 warning reduction创建新的 active topic并重新建立仓库根 clean build 真值。

View File

@ -0,0 +1,209 @@
# Analyzer Warning Reduction 追踪
## 2026-04-29 — RP-096
### 阶段:完成 `PR #301` 最终收尾并归档长期 warning-reduction 主题
- 触发背景:
- 用户要求先用 `$gframework-pr-review` 解决当前 PR review 的剩余问题,然后把整个长期分支主题归档
- 本轮 triage 结论:
- `MediatorArchitectureIntegrationTests` 并发更新、`YamlConfigConditionalSchemas` / `YamlConfigStringFormatConstraint``<exception>` 文档,以及两个枚举的 `[GenerateEnumExtensions]` 在当前工作树上均已存在,对应 open threads 判定为 stale
- `YamlConfigReferenceUsage.DisplayPath` 删除建议继续判定为不成立,因为 loader 诊断、引用索引和测试断言仍把它作为稳定语义标签使用
- `LoadAsync_Should_Accept_Empty_Object_Schema_Const` 失败仍然成立:上轮把 `YamlConfigAllowedValue` / `YamlConfigConstantValue``comparableValue` 收紧成 `ThrowIfNullOrWhiteSpace(...)` 后,误伤了空对象常量的合法空比较键
- 主线程实施:
- 将 `YamlConfigAllowedValue``YamlConfigConstantValue` 的比较键契约调整为:
- 允许 `string.Empty`
- 继续拒绝非空纯空白字符串
- 保留 `displayValue` 的非空白要求
- 扩充 `YamlConfigModelContractTests`,新增空比较键的正向覆盖,同时保留纯空白比较键的回归保护
- 验证里程碑:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
- 结果:成功;`10` 通过、`0` 失败
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
- 结果:成功
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
- 归档结论:
- `analyzer-warning-reduction` 当前 topic 已满足归档条件:长期 warning-reduction 主线已收尾PR #301 的本地 follow-up 闭环完成
- 整个 topic 目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,不再作为 active 默认入口
## 2026-04-29 — RP-095
### 阶段:复核 `PR #301` latest-head review threads并只修复当前工作树上仍然成立的问题
- 触发背景:
- 用户显式要求执行 `$gframework-pr-review`,需要把 GitHub PR review 信号与本地代码现状重新核对,而不是沿用旧的 warning-batch 假设
- 本轮 triage 结论:
- 接受并修复:
- `MediatorArchitectureIntegrationTests``Task.Delay().Wait()` 阻塞、静态 `Dictionary` 竞态、`SharedState.Counter +=` 非原子更新、以及 `TestNestedRequestHandler` 冗余分支
- `GFramework.Game/Config` 中仍然成立的模型契约缺口:空白比较键、数组 / 对象边界非法状态、`Pattern` / `PatternRegex` 不一致、`ReferencedTableNames` 未做 defensive copy、以及缺失的 `<exception>` XML 文档
- `MediatorAdvancedFeaturesTests``MA0048` 抑制缺少原因注释
- `YamlConfigSchemaNode.NodeValidation.None` 未被引用,按 review 建议删除死代码
- 明确不接受或延后:
- `YamlConfigReferenceUsage.DisplayPath`:当前在 loader 诊断与测试断言中承担独立语义标签,不作为“纯冗余 alias”删除
- `YamlConfigSchemaPropertyType` / `YamlConfigStringFormatKind``[GenerateEnumExtensions]`:仓库产品代码没有现成约定或使用面,判断为泛化误报
- 主线程实施:
- 将 CQRS 集成测试辅助处理器改为真正异步,并用 `ConcurrentDictionary` / `Interlocked` 收口并发共享状态
- 为 `YamlConfigAllowedValue``YamlConfigConstantValue``YamlConfigArrayContainsConstraints``YamlConfigArrayConstraints``YamlConfigObjectConstraints``YamlConfigStringConstraints``YamlConfigSchema``YamlConfigConditionalSchemas``YamlConfigStringFormatConstraint` 补运行时契约或 `<exception>` 注释
- 新增 `YamlConfigModelContractTests`,锁定上述模型拒绝无效状态的行为
- 验证里程碑:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"`
- 第一次结果:成功;`10` 通过、`0` 失败,但新增测试触发 `MA0009`
- 第二次结果:成功;`10` 通过、`0` 失败;为测试中的 `Regex` 补 timeout 后 warning 清零
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
- 结果:成功;`25` 通过、`0` 失败
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
- 下一步:
- 提交当前 PR-review follow-up 与 `ai-plan` 同步
- 推送后重新执行 `$gframework-pr-review`,确认 remaining open threads 是否已缩减到延后 / 误报项
## 2026-04-29 — RP-094
### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零
- 触发背景:
- 用户要求先拿构建 warning再在 warning 很多时分批指派 subagent本轮按 `$gframework-batch-boot 50` 继续执行
- 基线与停机判断:
- 当前 `origin/main` 仍为 `0e32dab``2026-04-28T17:15:47+08:00`
- 本轮标准仓库根 `dotnet clean` + `dotnet build` 直接成功warning 总数为 `15`,但全部集中在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
- 由于 `15` 条 warning 实际只对应同一文件内 `5` 个独立 `MA0051` 方法不满足“warning 非常多且可安全分派多个独立写边界”的条件,因此不再新增 worker
- 主线程实施:
- 将 `ParseNode` 拆成 `ResolveNodeTypeName``ValidateObjectOnlyKeywords``CreateParsedNodeForType`
- 将 `ValidateObjectNode` 拆成对象类型确认、属性遍历与 required 校验 helper
- 将 `ValidateObjectConstraints` 拆成 property count、`dependentRequired``dependentSchemas``allOf`、条件分支五个 helper
- 将 `ValidateScalarNode``ValidateNumericScalarConstraints` 分别拆成标量类型确认、引用回写、数值上下界和 `multipleOf` helper
- 追加 `ValidateConditionalSchemaBranch` 收口 if/then/else 分支;随后修正该 helper 引入的 `MA0006`
- 验证里程碑:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
- 第一次结果:成功;`3` warnings、`0` errors均为新 helper 中 `branchName == "then"` 引入的 `MA0006`
- 第二次结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
- 结果:成功;`80` 通过、`0` 失败
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
- 当前指标:
- 仓库根 clean build warning`15` -> `0`
- 当前分支相对 `origin/main...HEAD` 仍为 `22` 个变更文件,低于 `$gframework-batch-boot 50` 的文件阈值
- 当前停止原因warning hotspot 已耗尽,不再有可重复切片
- 下一步:
- 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 真值更新
## 2026-04-29 — RP-093
### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理
- 触发背景:
- 用户要求先拿构建 warning再分批指派 subagent 加快处理;停止条件解析为分支相对 `origin/main` 接近 `50` 个变更文件
- 基线与环境:
- 当前 `origin/main``0e32dab``2026-04-28T17:15:47+08:00`
- 标准 `dotnet clean` 在当前 WSL 环境仍被 Windows NuGet fallback package folder 阻塞;按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后,使用 `-p:RestoreFallbackFolders=` 完成 clean / build
- clean 后 warning 基线:`236` warnings、`0` errors
- 已接受的 worker 范围:
- `ed269d4``GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016`
- `121df44``GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015`
- `9109eec``GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015`
- 主线程实施:
- 在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 为固定格式正则与 schema `pattern` 正则补充 timeout避免运行时正则输入继续触发 `MA0009`
- 将三处字符串等值比较改为 ordinal `string.Equals`,清理 `MA0006`
- 接受 `1395b84``YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分,清理该文件 `MA0051`
- 收口被中止 worker 留下的 schema model 拆文件变更,将 `YamlConfigSchemaValidator.cs` 末尾类型移动到同名文件,清理 `MA0048`
- 验证里程碑:
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"`
- 结果:成功;`45` 通过、`0` 失败
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
- 结果:成功;`80` 通过、`0` 失败
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
- 结果:成功
- `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false`
- 中间结果:成功;`75` warnings、`0` errors
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
- 结果:成功
- `dotnet build -p:RestoreFallbackFolders= -clp:Summary -v:minimal -m:1 -nodeReuse:false`
- 结果:成功;`15 Warning(s)``0 Error(s)`
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
- 当前指标:
- warning 总数:`236` -> `15`
- 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs``MA0051` `15`5 个方法跨 3 个 TFM
- 本轮提交后预计分支 diff`22` 个文件,低于 `50` 个文件阈值
- 下一步:
- 按用户要求本轮到此结束;下一轮只处理 `YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分
## 2026-04-28 — RP-092
### 阶段:复核 `PR #300` 的 open threads并只修正当前分支仍然成立的 `ai-plan` 漂移
- 触发背景:
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
- 主线程实施:
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests``TestArchitectureWithRegistry``TestResourceLoader``PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op定向回归测试通过
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test``git diff --check`
- 验证里程碑:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
- 结果:成功;`10` 通过、`0` 失败
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
## 2026-04-28 — RP-091
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick并升级 PR-review triage 规则
- 触发背景:
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md``$gframework-pr-review`
- 主线程实施:
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
- 将 `TestArchitectureContext``TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`明确要求latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
- 验证里程碑:
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
- 结果:成功;`67` 通过、`0` 失败
- `git diff --check`
- 结果:成功;无新增 whitespace / conflict-marker 问题
## 活跃风险
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
## 下一步
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
## 历史归档指针
- 最新 trace 归档:
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- 历史 todo 归档:
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 早期归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)