fix(pr-review): 收敛PR建议并修复构建验证

- 修复 PR #288 中经本地复核后仍成立的 Core、Game 与测试建议

- 更新 WSL 标准 dotnet build 验证路径并确认 Release 构建可通过

- 补充 analyzer-warning-reduction 跟踪文档记录本轮结论与恢复点
This commit is contained in:
gewuyou 2026-04-25 14:26:49 +08:00
parent 70c42b579f
commit a7a3eca40d
19 changed files with 143 additions and 212 deletions

View File

@ -366,7 +366,7 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
@ -380,7 +380,7 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotSupportedException();
@ -393,7 +393,7 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
@ -407,7 +407,7 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotSupportedException();

View File

@ -401,7 +401,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
@ -414,7 +414,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <returns>请求响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotSupportedException();
@ -427,7 +427,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
@ -441,7 +441,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotSupportedException();
@ -454,7 +454,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
@ -468,7 +468,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotSupportedException();
@ -481,7 +481,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>通知发布任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
@ -495,7 +495,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
@ -510,7 +510,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令发送任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
@ -524,7 +524,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{

View File

@ -287,7 +287,7 @@ public class TestArchitectureContextWithRegistry : TestArchitectureContext
_registry = registry;
}
public override TUtility GetUtility<TUtility>()
public override TUtility? GetUtility<TUtility>() where TUtility : class
{
if (typeof(TUtility) == typeof(TestRegistry))
{

View File

@ -66,8 +66,6 @@ public class TaskCoroutineExtensionsTests
var task = Task.FromResult(42);
var instruction = task.AsCoroutineInstruction();
task.ConfigureAwait(false).GetAwaiter().GetResult();
Assert.That(instruction.Result, Is.EqualTo(42));
}

View File

@ -291,8 +291,7 @@ public class WaitForTaskTests
var wait = new WaitForTask<int>(task);
await task.ConfigureAwait(false);
Task.Delay(100).Wait();
await Task.Delay(100).ConfigureAwait(false);
Assert.That(wait.IsDone, Is.True);
Assert.That(wait.Result, Is.EqualTo(expectedValue));

View File

@ -68,7 +68,7 @@ public class RollingFileAppenderTests
}
// 检查是否生成了多个文件
var files = Directory.GetFiles(_testDir, "*.log").OrderBy(f => f, System.StringComparer.Ordinal).ToArray();
var files = Directory.GetFiles(_testDir, "*.log");
Assert.That(files.Length, Is.GreaterThan(1));
}

View File

@ -28,10 +28,7 @@ public static class AsyncExtensions
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
if (taskFactory is null)
{
throw new ArgumentNullException(nameof(taskFactory));
}
ArgumentNullException.ThrowIfNull(taskFactory);
// linkedCts 同时响应:超时 + 外部取消
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@ -74,10 +71,7 @@ public static class AsyncExtensions
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
if (taskFactory is null)
{
throw new ArgumentNullException(nameof(taskFactory));
}
ArgumentNullException.ThrowIfNull(taskFactory);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linkedCts.CancelAfter(timeout);
@ -119,15 +113,8 @@ public static class AsyncExtensions
/// </example>
public static async Task<T> WithFallbackAsync<T>(this Task<T> task, Func<Exception, T> fallback)
{
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
if (fallback is null)
{
throw new ArgumentNullException(nameof(fallback));
}
ArgumentNullException.ThrowIfNull(task);
ArgumentNullException.ThrowIfNull(fallback);
try
{

View File

@ -19,15 +19,8 @@ public static class ContextAwareCommandExtensions
public static TResult SendCommand<TResult>(this IContextAware contextAware,
ICommand<TResult> command)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (command is null)
{
throw new ArgumentNullException(nameof(command));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return context.SendCommand(command);
@ -41,15 +34,8 @@ public static class ContextAwareCommandExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 command 为 null 时抛出</exception>
public static void SendCommand(this IContextAware contextAware, ICommand command)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (command is null)
{
throw new ArgumentNullException(nameof(command));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
context.SendCommand(command);
@ -64,15 +50,8 @@ public static class ContextAwareCommandExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 command 为 null 时抛出</exception>
public static async Task SendCommandAsync(this IContextAware contextAware, IAsyncCommand command)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (command is null)
{
throw new ArgumentNullException(nameof(command));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
await context.SendCommandAsync(command).ConfigureAwait(false);
@ -89,15 +68,8 @@ public static class ContextAwareCommandExtensions
public static async Task<TResult> SendCommandAsync<TResult>(this IContextAware contextAware,
IAsyncCommand<TResult> command)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (command is null)
{
throw new ArgumentNullException(nameof(command));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(command);
var context = contextAware.GetContext();
return await context.SendCommandAsync(command).ConfigureAwait(false);

View File

@ -16,10 +16,7 @@ public static class ContextAwareEventExtensions
/// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception>
public static void SendEvent<TEvent>(this IContextAware contextAware) where TEvent : new()
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext();
context.SendEvent<TEvent>();
@ -34,15 +31,8 @@ public static class ContextAwareEventExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 e 为 null 时抛出</exception>
public static void SendEvent<TEvent>(this IContextAware contextAware, TEvent e) where TEvent : class
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(e);
var context = contextAware.GetContext();
context.SendEvent(e);
@ -57,15 +47,8 @@ public static class ContextAwareEventExtensions
/// <returns>事件注销接口</returns>
public static IUnRegister RegisterEvent<TEvent>(this IContextAware contextAware, Action<TEvent> handler)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(handler);
var context = contextAware.GetContext();
return context.RegisterEvent(handler);
@ -79,15 +62,8 @@ public static class ContextAwareEventExtensions
/// <param name="onEvent">之前绑定的事件处理器</param>
public static void UnRegisterEvent<TEvent>(this IContextAware contextAware, Action<TEvent> onEvent)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (onEvent is null)
{
throw new ArgumentNullException(nameof(onEvent));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(onEvent);
var context = contextAware.GetContext();
context.UnRegisterEvent(onEvent);

View File

@ -18,15 +18,8 @@ public static class ContextAwareQueryExtensions
/// <exception cref="ArgumentNullException">当 contextAware 或 query 为 null 时抛出</exception>
public static TResult SendQuery<TResult>(this IContextAware contextAware, IQuery<TResult> query)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (query is null)
{
throw new ArgumentNullException(nameof(query));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return context.SendQuery(query);
@ -44,15 +37,8 @@ public static class ContextAwareQueryExtensions
public static async Task<TResult> SendQueryAsync<TResult>(this IContextAware contextAware,
IAsyncQuery<TResult> query)
{
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
if (query is null)
{
throw new ArgumentNullException(nameof(query));
}
ArgumentNullException.ThrowIfNull(contextAware);
ArgumentNullException.ThrowIfNull(query);
var context = contextAware.GetContext();
return await context.SendQueryAsync(query).ConfigureAwait(false);

View File

@ -25,20 +25,9 @@ public static class NumericExtensions
/// </example>
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (min is null)
{
throw new ArgumentNullException(nameof(min));
}
if (max is null)
{
throw new ArgumentNullException(nameof(max));
}
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(min);
ArgumentNullException.ThrowIfNull(max);
if (min.CompareTo(max) > 0)
{

View File

@ -38,15 +38,8 @@ public static class StringExtensions
/// </example>
public static string Truncate(this string str, int maxLength, string suffix = "...")
{
if (str is null)
{
throw new ArgumentNullException(nameof(str));
}
if (suffix is null)
{
throw new ArgumentNullException(nameof(suffix));
}
ArgumentNullException.ThrowIfNull(str);
ArgumentNullException.ThrowIfNull(suffix);
if (maxLength < suffix.Length)
{
@ -77,15 +70,8 @@ public static class StringExtensions
/// </example>
public static string Join(this IEnumerable<string> values, string separator)
{
if (values is null)
{
throw new ArgumentNullException(nameof(values));
}
if (separator is null)
{
throw new ArgumentNullException(nameof(separator));
}
ArgumentNullException.ThrowIfNull(values);
ArgumentNullException.ThrowIfNull(separator);
return string.Join(separator, values);
}

View File

@ -39,10 +39,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
{
if (middleware is null)
{
throw new ArgumentNullException(nameof(middleware));
}
ArgumentNullException.ThrowIfNull(middleware);
_configurators.Add(store => store.UseMiddleware(middleware));
return this;
@ -112,10 +109,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer)
{
if (reducer is null)
{
throw new ArgumentNullException(nameof(reducer));
}
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
@ -129,10 +123,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer)
{
if (reducer is null)
{
throw new ArgumentNullException(nameof(reducer));
}
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;

View File

@ -24,18 +24,11 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// </summary>
private readonly List<SelectionListenerSubscription> _listeners = [];
/// <summary>
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
/// </summary>
#if NET9_0_OR_GREATER
/// <summary>
/// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
/// </summary>
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
/// <summary>
/// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
/// </summary>
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif

View File

@ -193,7 +193,7 @@ public abstract class SceneRouterBase
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePushCoreAsync(@event, sceneKey, param)).ConfigureAwait(false);
() => ExecutePushCoreAsync(@event, sceneKey, param)).ConfigureAwait(true);
}
finally
{
@ -271,7 +271,7 @@ public abstract class SceneRouterBase
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePopCoreAsync(@event)).ConfigureAwait(false);
() => ExecutePopCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -339,7 +339,7 @@ public abstract class SceneRouterBase
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecuteClearCoreAsync(@event)).ConfigureAwait(false);
() => ExecuteClearCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -357,7 +357,7 @@ public abstract class SceneRouterBase
{
while (Stack.Count > 0)
{
await PopInternalAsync();
await PopInternalAsync().ConfigureAwait(true);
}
}
@ -365,6 +365,8 @@ public abstract class SceneRouterBase
#region Helper Methods
// Scene 生命周期回调和 pipeline handlers 可能依赖引擎线程,因此这些核心切换顺序统一显式保留上下文。
/// <summary>
/// 执行 Replace 的核心切换顺序。
/// </summary>
@ -377,11 +379,10 @@ public abstract class SceneRouterBase
string sceneKey,
ISceneEnterParam? param)
{
// 场景生命周期回调可能依赖引擎线程,因此这里保留默认 await 行为。
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
@ -396,9 +397,9 @@ public abstract class SceneRouterBase
string sceneKey,
ISceneEnterParam? param)
{
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
@ -408,9 +409,9 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
private async Task ExecutePopCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event);
await PopInternalAsync();
await AfterChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PopInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
@ -420,9 +421,9 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
private async Task ExecuteClearCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await AfterChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
@ -454,7 +455,7 @@ public abstract class SceneRouterBase
private async Task BeforeChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange).ConfigureAwait(false);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange).ConfigureAwait(true);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
@ -465,7 +466,7 @@ public abstract class SceneRouterBase
private async Task AfterChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange).ConfigureAwait(false);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange).ConfigureAwait(true);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}

View File

@ -251,7 +251,7 @@ public sealed class FileStorage : IFileStorage, IDisposable
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
using var sr = new StreamReader(fs, Encoding.UTF8);
using var sr = new StreamReader(fs, Encoding.UTF8, true, -1, leaveOpen: true);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);
return _serializer.Deserialize<T>(content);
}
@ -373,7 +373,7 @@ public sealed class FileStorage : IFileStorage, IDisposable
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
var sw = new StreamWriter(fs, Encoding.UTF8);
var sw = new StreamWriter(fs, Encoding.UTF8, leaveOpen: true);
await using var configuredStreamWriter = sw.ConfigureAwait(false);
await sw.WriteAsync(content).ConfigureAwait(false);

View File

@ -100,9 +100,9 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
/// </summary>
/// <param name="key">要删除的键</param>
/// <returns>异步操作任务</returns>
public async Task DeleteAsync(string key)
public Task DeleteAsync(string key)
{
await inner.DeleteAsync(Key(key)).ConfigureAwait(false);
return inner.DeleteAsync(Key(key));
}
/// <summary>

View File

@ -6,15 +6,16 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-063`
- 当前阶段:`Phase 63`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-064`
- 当前阶段:`Phase 64`
- 当前焦点:
- `2026-04-25` 当前 turn 先执行 `$gframework-pr-review`,核对 PR #288 的 latest-head AI review 与本地真实状态
- 已修复 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs``ConfiguredTaskAwaitable -> Task` 编译错误,并顺手收敛一批同类测试/样式残留
- `2026-04-25` 当前 turn 先执行 `$gframework-pr-review`,复核 PR #288 的 latest-head unresolved 线程与折叠评论
- 已收敛一批经本地复核后仍成立的 review 建议,包括 `ThrowIfNull` 回退、测试桩 XML 注释修正、`FileStorage` 资源所有权、`SceneRouterBase` 线程亲和语义与若干测试噪音
- 已确认用户在 WSL 下直接执行的标准 `dotnet build -c Release` 路径可用;前一轮失败主要来自主线程附加的 workaround 参数而非仓库本身不可构建
- 基线 `origin/main` 仍为 `9964962``2026-04-24T23:05:53+08:00`
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行,已触达本轮 `75 files` 阈值
- `RP-061` 之后已接受 2 个批次提交:`03c73a8``9ce1fa6`
- 当前默认恢复入口不再继续扩写集;若要继续 analyzer reduction优先先处理 WSL 下 NuGet fallback package folder 指向失效 Windows 路径的构建环境阻塞
- 当前默认恢复入口不再继续扩写集;若要继续 analyzer reduction优先重新抓取 PR #288 的 unresolved 线程并按最新 head 再做一轮收口
## 当前活跃事实
@ -30,6 +31,10 @@
- `PauseStackManagerTests.cs`
- 本 turn 结合 PR #288 latest-head review 额外收敛了以下仍然成立的问题:
- `AsyncExtensionsTests.cs`:修复 `WithTimeoutAsync` 无返回值测试中错误返回 `ConfiguredTaskAwaitable` 导致的 `CS0029` / `CS1662`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `AsyncExtensions.cs`
- `AsyncKeyLockManagerTests.cs`:去掉两处不会产生额外价值的 `Assert.DoesNotThrowAsync(() => Task.WhenAll(...))` 包装,并把取消断言改为直接消费 `ValueTask.AsTask()`
- `AsyncArchitectureTests.cs`
- `ArchitectureLifecycleBehaviorTests.cs`
@ -39,11 +44,19 @@
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `ArchitectureServicesTests.cs`
- `GameContextTests.cs`
- `RollingFileAppenderTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTests.cs`
- `ScopedStorage.cs`
- `FileStorage.cs`
- `SceneRouterBase.cs`
- 当前 PR review 观察:
- PR`#288`
- latest reviewed commit`be336b2088b7c283a140add76d5cff30618ad16d`
- `coderabbitai[bot]` 仍有 `7` 个 open threads`greptile-apps[bot]` 仍有 `2` 个 open threads
- 本 turn 已优先修复 latest-head 中明确指向 `AsyncExtensionsTests.cs:126` 的 critical 编译错误
- latest reviewed commit`70c42b579f70c90ab5461a02e611c0fbd8d8a6f2`
- 抓取时 `coderabbitai[bot]``6` 个 open threads`greptile-apps[bot]``2` 个 open threads
- `Actionable comments posted: 7``outside diff + nitpick = 19` 并不等于必须全收;本 turn 仅接受经本地复核后仍成立且不与仓库约束冲突的建议
- 本轮 `Core` runtime 低风险机械型清理已落地到:
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
@ -64,15 +77,14 @@
## 当前风险
- 当前环境下 `GFramework.Core` / `GFramework.Core.Tests` 的 Release build 会命中 `MSB4018`
- 直接原因:`ResolvePackageAssets` 仍从历史 restore 生成的 `obj/*.csproj.nuget.g.props` 读取失效的 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- 缓解措施:下次恢复时先重建 WSL 原生 restore 元数据,或显式清理并重做 `obj` 下的 NuGet restore 工件,再重新建立可信 build 基线。
- `dotnet clean GFramework.sln -c Release``dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 仍无法稳定提供新的 clean 基线。
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真值。
- 当前 worktree 仍存在未跟踪的 `.codex` 目录。
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交。
- 将分支继续推过 `75 files` 会明显降低本轮 reviewability。
- 缓解措施:当前恢复点默认停止;如需继续,建议在新 turn 明确新的文件阈值或先 rebase / refresh baseline。
- `GFramework.Core``GFramework.Game``GFramework.Core.Tests` 当前都仍存在模块级历史 warning 基线。
- 缓解措施:本 turn 已确保本次 touched files 不再引入新的编译错误,并消化了当前 PR review 中仍成立的高信号问题;若要继续 warning reduction应开新批次按模块系统化收敛。
## 活跃文档
@ -100,6 +112,14 @@
- 结果:失败;`MSB4018`,原因同上
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功;定位到 PR `#288`,提取 latest-head unresolved AI review threads、MegaLinter 与 Docstring Coverage 信号
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;已刷新 WSL 原生 restore 元数据,清除先前的 stale fallback package folder 阻塞
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,因此不能用 `net8.0` 旁路验证 `Core.Tests`
- `git diff --name-only origin/main...HEAD | wc -l`
@ -109,6 +129,6 @@
## 下一步建议
1. 当前 turn 已先修复 latest-head PR review 中最紧急的编译错误;后续若继续 PR #288 收尾,优先重新抓取 unresolved threads确认剩余 8 个 open threads 哪些仍成立
2. 若后续要继续 `Core` / `Core.Tests` warning reduction先修复 WSL 下 stale NuGet restore metadata 导致的 `MSB4018`,再重新建立可信 build 基线
1. 当前 turn 已按标准 WSL `dotnet build` 路径完成 `Core` / `Game` / `Core.Tests` Release build 验证;后续若继续 PR #288 收尾,优先重新抓取 unresolved threads确认哪些线程已可直接 resolve
2. 若后续要继续 `Core` / `Core.Tests` / `Game` warning reduction应以当前标准 build 输出为新真值,而不是继续沿用上一轮带 workaround 参数的失败命令
3. 若要开启下一轮批处理,优先选择新的 stop-condition例如新的 file 阈值、warning 目标或限定到单模块)后再继续。

View File

@ -1,5 +1,38 @@
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-064
### 阶段:按标准 WSL build 路径复核 PR #288 建议并完成本轮收口
- 触发背景:
- 用户指出“在 WSL 里直接执行 `dotnet build` 可以成功”,要求主线程按普通路径重新验证,而不是继续使用带 `MSBuildEnableWorkloadResolver=false``--no-restore`、手工 `TargetFramework` 的 workaround 命令
- 当前任务仍属于 PR #288 review follow-up因此本轮重点改为“区分哪些 AI 建议值得采纳”以及“用真实 WSL build 结果验证”
- 主线程实施:
- 重新抓取 PR #288 review确认 latest-head open threads 为 `CodeRabbit 6 + Greptile 2`
- 复核 `outside diff + nitpick` 的 19 条建议,只采纳本地仍成立的建议;拒绝把“评论总数”机械等同于“必须全改”
- 完成以下高信号修复:
- `ContextAware*` / `AsyncExtensions` / `NumericExtensions` / `StringExtensions` / `StoreBuilder`:回退为 `ArgumentNullException.ThrowIfNull(...)`
- `ArchitectureServicesTests` / `GameContextTests`:同步 XML `<exception>``NotSupportedException`
- `RegistryInitializationHookBaseTests`:修复 override 可空签名实现,避免再次引入编译错误
- `RollingFileAppenderTests` / `TaskCoroutineExtensionsTests` / `WaitForTaskTests` / `ScopedStorage`:移除无收益噪音代码
- `FileStorage`:通过 `leaveOpen: true` 修正 `FileStream` 的双重释放语义
- `SceneRouterBase`:统一显式 `ConfigureAwait(true)` 并补齐引擎线程亲和说明
- `StoreSelection`:保留 `net9.0+``System.Threading.Lock`,同时修正条件编译旁的注释写法,避免 `CS1587`
- 验证里程碑:
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;证明先前 `MSB4018` 来自 stale restore 元数据,而不是当前 WSL 默认 build 路径本身不可用
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- 当前结论:
- 用户关于“WSL 里直接 `dotnet build` 可行”的判断正确
- 前一轮失败的核心原因不是仓库不可构建,而是主线程附加的 workaround 参数改变了 MSBuild 行为
- 本轮已完成 PR #288 中一组仍成立的建议修复,并重新拿到标准 WSL 路径下的 Release build 验证
- 剩余 review 线程需要在新 head 上重新抓取后再决定是否逐条 resolve
## 2026-04-25 — RP-063
### 阶段:先收口 PR #288 latest-head 编译错误,再暂停在环境阻塞点并准备提交