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

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-26 09:01:04 +08:00 committed by GitHub
commit 0c7552e629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 927 additions and 589 deletions

1
.gitignore vendored
View File

@ -23,5 +23,6 @@ ai-plan/public/*
!ai-plan/public/**/*.md
ai-plan/private/
ai-libs/
.codex
# tool
.venv/

View File

@ -33,6 +33,13 @@ All AI agents and contributors must follow these rules when writing, reviewing,
baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
a clean baseline has not been captured in the same round.
- If a direct `dotnet clean`, `dotnet build`, or `dotnet test` command fails inside the agent sandbox with missing
diagnostics, `Permission denied`, MSBuild pipe/socket errors, or other environment-only noise that does not match a
normal shell invocation, contributors MUST request permission and rerun the same direct command outside the sandbox
before concluding that the repository or toolchain is broken.
- For repository truth, contributors MUST prefer the result of the original direct command executed outside the sandbox
over sandbox-only failures, workaround-heavy variants, or speculative environment flags unless the user explicitly
asks for a non-default command shape.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
@ -235,6 +242,8 @@ All generated or modified code MUST include clear and meaningful comments where
### Validation Commands
Use the smallest command set that proves the change, then expand if the change is cross-cutting.
If a sandboxed agent run reports environment-specific .NET failures, rerun the same direct command outside the sandbox
and treat that unsandboxed result as authoritative for validation and warning baselines.
```bash
# Check warnings from the default repository build entrypoint

View File

@ -157,7 +157,7 @@ public class LogContextTests
using (LogContext.Push("TaskId", "Task1"))
{
task1Values.Add(LogContext.Current["TaskId"]);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
task1Values.Add(LogContext.Current["TaskId"]);
}
});
@ -167,12 +167,12 @@ public class LogContextTests
using (LogContext.Push("TaskId", "Task2"))
{
task2Values.Add(LogContext.Current["TaskId"]);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
task2Values.Add(LogContext.Current["TaskId"]);
}
});
await Task.WhenAll(task1, task2);
await Task.WhenAll(task1, task2).ConfigureAwait(false);
Assert.That(task1Values, Has.All.EqualTo("Task1"));
Assert.That(task2Values, Has.All.EqualTo("Task2"));

View File

@ -425,6 +425,8 @@ public class LoggerTests
/// </summary>
public sealed class TestLogger : AbstractLogger
{
private readonly List<LogEntry> _logs = new();
/// <summary>
/// 初始化TestLogger的新实例
/// </summary>
@ -435,9 +437,9 @@ public sealed class TestLogger : AbstractLogger
}
/// <summary>
/// 获取记录的日志条目列表
/// 获取按写入顺序保存的日志条目只读视图
/// </summary>
public List<LogEntry> Logs { get; } = new();
public IReadOnlyList<LogEntry> Logs => _logs;
/// <summary>
/// 将日志信息写入内部存储
@ -447,7 +449,7 @@ public sealed class TestLogger : AbstractLogger
/// <param name="exception">相关异常(可选)</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
Logs.Add(new LogEntry(level, message, exception));
_logs.Add(new LogEntry(level, message, exception));
}
/// <summary>
@ -457,4 +459,4 @@ public sealed class TestLogger : AbstractLogger
/// <param name="Message">日志消息</param>
/// <param name="Exception">相关异常(可选)</param>
public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception);
}
}

View File

@ -327,27 +327,7 @@ internal sealed class CqrsHandlerRegistrarTests
[Test]
public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
var generatedAssembly = CreateCachedMetadataAssembly();
var firstContainer = new MicrosoftDiContainer();
var secondContainer = new MicrosoftDiContainer();
@ -356,43 +336,8 @@ internal sealed class CqrsHandlerRegistrarTests
firstContainer.Freeze();
secondContainer.Freeze();
var firstRegistrations = firstContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
var secondRegistrations = secondContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
Assert.Multiple(() =>
{
Assert.That(
firstRegistrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
Assert.That(
secondRegistrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
});
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
AssertGeneratedRegistryNotificationHandlers(firstContainer, secondContainer);
VerifyCachedMetadataAssemblyLookups(generatedAssembly);
}
/// <summary>
@ -526,6 +471,93 @@ internal sealed class CqrsHandlerRegistrarTests
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
}
/// <summary>
/// 创建一个携带 generated registry 与 reflection fallback 元数据的程序集替身,
/// 用于验证 registrar 是否会跨容器复用程序集级元数据。
/// </summary>
private static Mock<Assembly> CreateCachedMetadataAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
return generatedAssembly;
}
/// <summary>
/// 断言两个容器都获得了相同的 generated-registry 与 reflection-fallback 处理器集合。
/// </summary>
private static void AssertGeneratedRegistryNotificationHandlers(
MicrosoftDiContainer firstContainer,
MicrosoftDiContainer secondContainer)
{
var firstRegistrations = GetGeneratedRegistryNotificationHandlerTypes(firstContainer);
var secondRegistrations = GetGeneratedRegistryNotificationHandlerTypes(secondContainer);
Assert.Multiple(() =>
{
Assert.That(firstRegistrations, Is.EqualTo(GetExpectedGeneratedRegistryNotificationHandlerTypes()));
Assert.That(secondRegistrations, Is.EqualTo(GetExpectedGeneratedRegistryNotificationHandlerTypes()));
});
}
/// <summary>
/// 读取容器中针对 generated notification 的 handler 运行时类型列表。
/// </summary>
private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container)
{
return container.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
}
/// <summary>
/// 获取 generated registry 与 reflection fallback 共同组成的预期 handler 顺序。
/// </summary>
private static Type[] GetExpectedGeneratedRegistryNotificationHandlerTypes()
{
return
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
];
}
/// <summary>
/// 断言程序集级 generated registry / fallback 元数据只会被读取一次。
/// </summary>
private static void VerifyCachedMetadataAssemblyLookups(Mock<Assembly> generatedAssembly)
{
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
}
/// <summary>
/// 通过反射读取 registrar 的静态缓存对象。
/// </summary>

View File

@ -21,6 +21,8 @@ namespace GFramework.Cqrs.Tests.Logging;
/// </summary>
public sealed class TestLogger : AbstractLogger
{
private readonly List<LogEntry> _logs = [];
/// <summary>
/// 初始化测试日志记录器。
/// </summary>
@ -33,7 +35,7 @@ public sealed class TestLogger : AbstractLogger
/// <summary>
/// 获取当前测试期间捕获到的日志条目。
/// </summary>
public List<LogEntry> Logs { get; } = [];
public IReadOnlyList<LogEntry> Logs => _logs;
/// <summary>
/// 将日志写入内存,供断言使用。
@ -43,7 +45,7 @@ public sealed class TestLogger : AbstractLogger
/// <param name="exception">关联异常。</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
Logs.Add(new LogEntry(level, message, exception));
_logs.Add(new LogEntry(level, message, exception));
}
/// <summary>

View File

@ -366,11 +366,6 @@ public sealed class TestBehaviorRequestHandler : IRequestHandler<TestBehaviorReq
}
}
public static class TestLoggingBehavior
{
public static List<string> LoggedMessages { get; set; } = new();
}
public sealed record TestValidatedRequest : IRequest<string>
{
public int Value { get; init; }
@ -467,8 +462,19 @@ public sealed record TestCircuitBreakerRequest : IRequest<string>
// 复杂场景相关类
public class SagaData
{
public List<int> CompletedSteps { get; } = new();
public List<int> CompensatedSteps { get; } = new();
/// <summary>
/// 获取 Saga 已成功执行的步骤集合。
/// </summary>
public IList<int> CompletedSteps { get; } = new List<int>();
/// <summary>
/// 获取 Saga 失败后已执行补偿的步骤集合。
/// </summary>
public IList<int> CompensatedSteps { get; } = new List<int>();
/// <summary>
/// 获取或设置 Saga 是否已经完整结束。
/// </summary>
public bool IsCompleted { get; set; }
}
@ -489,7 +495,11 @@ public sealed record TestExternalServiceRequest : IRequest<string>
public sealed record TestDatabaseRequest : IRequest<string>
{
public string Data { get; init; } = string.Empty;
public List<string> Storage { get; init; } = new();
/// <summary>
/// 获取或初始化用于模拟数据库写入的可变存储集合,同时避免泄漏具体集合实现。
/// </summary>
public IList<string> Storage { get; init; } = new List<string>();
}
#endregion

View File

@ -60,11 +60,8 @@ public class EcsAdvancedTests
InitializeEcsModule();
_world!.Create(new Position(0, 0));
Assert.DoesNotThrow(() =>
{
World.Destroy(_world);
_world = null;
});
World.Destroy(_world);
_world = null;
}
[Test]
@ -124,7 +121,7 @@ public class EcsAdvancedTests
_world = _container!.Get<World>();
Assert.DoesNotThrow(() => _ecsModule.Update(1.0f));
_ecsModule.Update(1.0f);
}
[Test]
@ -211,4 +208,4 @@ public class EcsAdvancedTests
Assert.That(_world.Has<Position>(entity3), Is.False);
Assert.That(_world.Has<Velocity>(entity3), Is.True);
}
}
}

View File

@ -167,7 +167,7 @@ public class GameConfigBootstrapTests
continueInitialization.Set();
Assert.DoesNotThrowAsync(() => firstInitializeTask);
Assert.That(async () => await firstInitializeTask.ConfigureAwait(false), Throws.Nothing);
Assert.Multiple(() =>
{

View File

@ -198,11 +198,10 @@ public class GeneratedConfigConsumerIntegrationTests
Assert.That(yaml.EndsWith("\n", StringComparison.Ordinal), Is.True);
});
Assert.DoesNotThrow(() =>
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml));
Assert.DoesNotThrowAsync(async () =>
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml).ConfigureAwait(false));
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml);
Assert.That(
async () => await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml).ConfigureAwait(false),
Throws.Nothing);
var invalidYaml = """
id: 3

View File

@ -1,4 +1,5 @@
using System.IO;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
@ -2789,83 +2790,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
dropItemIds:
- potion
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "dropItemIds"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemIds": {
"type": "array",
"minContains": 1,
"contains": {
"type": "string",
"x-gframework-ref-table": "item"
},
"items": {
"type": "string"
}
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedContainsReferenceHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateConfigFile(
"item/potion.yaml",
"""
id: elixir
name: Elixir
""");
CreateConfigFile("item/potion.yaml", UpdatedItemConfigContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -2958,65 +2891,17 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Support_Options_Object()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
});
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName),
DebounceDelay = TimeSpan.FromMilliseconds(150)
});
var (loader, registry) = await CreateLoadedMonsterHotReloadScenarioAsync(useOptionsObject: true)
.ConfigureAwait(false);
var (reloadTaskSource, hotReload) = EnableHotReloadWithReloadCapture(loader, registry, useOptionsObject: true);
try
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 25
""");
CreateConfigFile("monster/slime.yaml", UpdatedMonsterConfigContent);
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5));
Assert.Multiple(() =>
{
Assert.That(tableName, Is.EqualTo("monster"));
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(25));
});
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
AssertMonsterHotReloadUpdated(tableName, registry);
}
finally
{
@ -3050,60 +2935,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_Table_When_Schema_Change_Makes_Reload_Fail()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedMonsterHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "rarity"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" },
"rarity": { "type": "string" }
}
}
""");
CreateSchemaFile("schemas/monster.schema.json", MonsterSchemaWithRarityContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -3130,75 +2970,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_State_When_Dependency_Table_Breaks_Cross_Table_Reference()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
dropItemId: potion
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "dropItemId"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedCrossTableReferenceHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateConfigFile(
"item/potion.yaml",
"""
id: elixir
name: Elixir
""");
CreateConfigFile("item/potion.yaml", UpdatedItemConfigContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -3223,6 +3003,243 @@ public class YamlConfigLoaderTests
}
}
private const string ItemSchemaContent =
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""";
private const string InitialMonsterConfigContent =
"""
id: 1
name: Slime
hp: 10
""";
private const string UpdatedMonsterConfigContent =
"""
id: 1
name: Slime
hp: 25
""";
private const string MonsterSchemaContent =
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""";
private const string MonsterSchemaWithRarityContent =
"""
{
"type": "object",
"required": ["id", "name", "rarity"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" },
"rarity": { "type": "string" }
}
}
""";
private const string UpdatedItemConfigContent =
"""
id: elixir
name: Elixir
""";
private const string MonsterDropArrayConfigContent =
"""
id: 1
name: Slime
dropItemIds:
- potion
""";
private const string MonsterDropArraySchemaContent =
"""
{
"type": "object",
"required": ["id", "name", "dropItemIds"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemIds": {
"type": "array",
"minContains": 1,
"contains": {
"type": "string",
"x-gframework-ref-table": "item"
},
"items": {
"type": "string"
}
}
}
}
""";
private const string MonsterDropConfigContent =
"""
id: 1
name: Slime
dropItemId: potion
""";
private const string MonsterDropSchemaContent =
"""
{
"type": "object",
"required": ["id", "name", "dropItemId"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""";
/// <summary>
/// 创建并加载标准 monster 热重载夹具,供重载成功与 schema 失败场景复用。
/// </summary>
/// <param name="useOptionsObject">是否通过选项对象注册表。</param>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)> CreateLoadedMonsterHotReloadScenarioAsync(
bool useOptionsObject = false)
{
CreateConfigFile("monster/slime.yaml", InitialMonsterConfigContent);
CreateSchemaFile("schemas/monster.schema.json", MonsterSchemaContent);
var loader = useOptionsObject
? new YamlConfigLoader(_rootPath).RegisterTable(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
})
: new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 创建并加载 contains 子 schema 引用场景,供热重载依赖回滚测试复用。
/// </summary>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)>
CreateLoadedContainsReferenceHotReloadScenarioAsync()
{
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
MonsterDropArrayConfigContent,
MonsterDropArraySchemaContent,
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 创建并加载跨表单值引用场景,供热重载依赖回滚测试复用。
/// </summary>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)>
CreateLoadedCrossTableReferenceHotReloadScenarioAsync()
{
var loader = CreateItemBackedMonsterLoader<MonsterDropConfigStub>(
MonsterDropConfigContent,
MonsterDropSchemaContent,
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 以统一的失败回调配置启用热重载,避免每个测试重复接线相同的通知逻辑。
/// </summary>
/// <param name="loader">已完成首次加载的加载器。</param>
/// <param name="registry">要复用的配置注册表。</param>
/// <returns>失败通知任务源与取消注册句柄。</returns>
private static (TaskCompletionSource<(string TableName, Exception Exception)> TaskSource, IUnRegister Registration)
EnableHotReloadWithFailureCapture(YamlConfigLoader loader, ConfigRegistry registry)
{
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
return (reloadFailureTaskSource, hotReload);
}
/// <summary>
/// 以统一的成功回调配置启用热重载,避免相同的防抖与回调装配在测试中重复出现。
/// </summary>
/// <param name="loader">已完成首次加载的加载器。</param>
/// <param name="registry">要复用的配置注册表。</param>
/// <param name="useOptionsObject">是否通过选项对象启用热重载。</param>
/// <returns>成功通知任务源与取消注册句柄。</returns>
private static (TaskCompletionSource<string> TaskSource, IUnRegister Registration) EnableHotReloadWithReloadCapture(
YamlConfigLoader loader,
ConfigRegistry registry,
bool useOptionsObject = false)
{
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var hotReload = useOptionsObject
? loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName),
DebounceDelay = TimeSpan.FromMilliseconds(150)
})
: loader.EnableHotReload(
registry,
onTableReloaded: tableName => reloadTaskSource.TrySetResult(tableName),
debounceDelay: TimeSpan.FromMilliseconds(150));
return (reloadTaskSource, hotReload);
}
/// <summary>
/// 断言标准 monster 热重载成功后,通知表名与刷新后的生命值都符合预期。
/// </summary>
/// <param name="tableName">热重载回调返回的表名。</param>
/// <param name="registry">承载刷新结果的注册表。</param>
private static void AssertMonsterHotReloadUpdated(string tableName, ConfigRegistry registry)
{
Assert.Multiple(() =>
{
Assert.That(tableName, Is.EqualTo("monster"));
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(25));
});
}
/// <summary>
/// 为对象数组 <c>contains</c> 子集匹配场景创建加载器,避免测试方法体被大段固定 schema 稀释。
/// </summary>

View File

@ -54,16 +54,15 @@ public sealed class YamlConfigTextValidatorTests
}
""");
Assert.DoesNotThrow(() =>
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
"""));
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
""");
}
/// <summary>
@ -169,8 +168,7 @@ public sealed class YamlConfigTextValidatorTests
hp: 10
""";
Assert.DoesNotThrow(() =>
YamlConfigTextValidator.Validate("monster", schemaPath, "monster/generated.yaml", yaml));
YamlConfigTextValidator.Validate("monster", schemaPath, "monster/generated.yaml", yaml);
File.WriteAllText(
schemaPath,

View File

@ -23,6 +23,19 @@ namespace GFramework.Game.Internal;
/// </remarks>
internal static class VersionedMigrationRunner
{
/// <summary>
/// 复用一次迁移链执行期间不会变化的上下文,避免多个 helper 重复传递同一组委托和消息元数据。
/// </summary>
/// <typeparam name="TData">迁移数据类型。</typeparam>
/// <typeparam name="TMigration">迁移描述类型。</typeparam>
private readonly record struct MigrationExecutionContext<TData, TMigration>(
Func<TData, int> GetVersion,
Func<TMigration, int> GetToVersion,
Func<TMigration, TData, TData> ApplyMigration,
string SubjectName,
string MigrationKind)
where TData : class;
/// <summary>
/// 校验迁移注册是否表示一次有效的前向升级。
/// </summary>
@ -92,58 +105,154 @@ internal static class VersionedMigrationRunner
ArgumentException.ThrowIfNullOrWhiteSpace(migrationKind);
var currentVersion = getVersion(data);
if (currentVersion > targetVersion)
{
throw new InvalidOperationException(
$"{subjectName} is version {currentVersion}, which is newer than the current runtime version {targetVersion}.");
}
EnsureRuntimeVersionIsSupported(currentVersion, targetVersion, subjectName);
if (currentVersion == targetVersion)
{
return data;
}
var context = new MigrationExecutionContext<TData, TMigration>(
getVersion,
getToVersion,
applyMigration,
subjectName,
migrationKind);
var current = data;
while (currentVersion < targetVersion)
{
var migration = resolveMigration(currentVersion);
if (migration is null)
{
throw new InvalidOperationException(
$"No {migrationKind} is registered for {subjectName} from version {currentVersion}.");
}
current = applyMigration(migration, current)
?? throw new InvalidOperationException(
$"{migrationKind} for {subjectName} from version {currentVersion} returned null.");
var migratedVersion = getVersion(current);
var declaredTargetVersion = getToVersion(migration);
if (declaredTargetVersion != migratedVersion)
{
throw new InvalidOperationException(
$"{migrationKind} for {subjectName} declared target version {declaredTargetVersion} " +
$"but returned version {migratedVersion}.");
}
if (migratedVersion <= currentVersion)
{
throw new InvalidOperationException(
$"{migrationKind} for {subjectName} must advance beyond version {currentVersion}.");
}
if (migratedVersion > targetVersion)
{
throw new InvalidOperationException(
$"{migrationKind} for {subjectName} produced version {migratedVersion}, " +
$"which exceeds the current runtime version {targetVersion}.");
}
currentVersion = migratedVersion;
var migration = GetRequiredMigration(resolveMigration, currentVersion, in context);
var result = ApplyMigrationStep(
migration,
current,
currentVersion,
targetVersion,
in context);
current = result.Data;
currentVersion = result.Version;
}
return current;
}
/// <summary>
/// 拒绝比当前运行时更高的数据版本,避免迁移器在未知版本上继续执行。
/// </summary>
/// <param name="currentVersion">数据当前版本。</param>
/// <param name="targetVersion">运行时支持的目标版本。</param>
/// <param name="subjectName">迁移主体名称。</param>
/// <exception cref="InvalidOperationException">数据版本高于运行时版本时抛出。</exception>
private static void EnsureRuntimeVersionIsSupported(int currentVersion, int targetVersion, string subjectName)
{
if (currentVersion > targetVersion)
{
throw new InvalidOperationException(
$"{subjectName} is version {currentVersion}, which is newer than the current runtime version {targetVersion}.");
}
}
/// <summary>
/// 解析当前版本必须存在的下一步迁移器,避免在调用循环中重复拼接相同错误。
/// </summary>
/// <typeparam name="TData">迁移数据类型。</typeparam>
/// <typeparam name="TMigration">迁移描述类型。</typeparam>
/// <param name="resolveMigration">迁移解析委托。</param>
/// <param name="currentVersion">当前版本。</param>
/// <param name="context">本轮迁移链共享的执行上下文。</param>
/// <returns>已解析的迁移器。</returns>
/// <exception cref="InvalidOperationException">缺少迁移器时抛出。</exception>
private static TMigration GetRequiredMigration<TData, TMigration>(
Func<int, TMigration?> resolveMigration,
int currentVersion,
in MigrationExecutionContext<TData, TMigration> context)
where TData : class
{
var migration = resolveMigration(currentVersion);
if (migration is null)
{
throw new InvalidOperationException(
$"No {context.MigrationKind} is registered for {context.SubjectName} from version {currentVersion}.");
}
return migration;
}
/// <summary>
/// 执行单步迁移并验证声明目标版本、结果版本与运行时上限之间的一致性。
/// </summary>
/// <typeparam name="TData">迁移数据类型。</typeparam>
/// <typeparam name="TMigration">迁移描述类型。</typeparam>
/// <param name="migration">当前步骤的迁移器。</param>
/// <param name="current">迁移前数据。</param>
/// <param name="currentVersion">迁移前版本。</param>
/// <param name="targetVersion">运行时目标版本。</param>
/// <param name="context">本轮迁移链共享的执行上下文。</param>
/// <returns>迁移后的数据与新版本号。</returns>
/// <exception cref="InvalidOperationException">
/// 迁移结果为空、声明目标版本不匹配、版本未前进或超出运行时版本时抛出。
/// </exception>
private static (TData Data, int Version) ApplyMigrationStep<TData, TMigration>(
TMigration migration,
TData current,
int currentVersion,
int targetVersion,
in MigrationExecutionContext<TData, TMigration> context)
where TData : class
{
var migratedData = context.ApplyMigration(migration, current)
?? throw new InvalidOperationException(
$"{context.MigrationKind} for {context.SubjectName} from version {currentVersion} returned null.");
var migratedVersion = context.GetVersion(migratedData);
ValidateMigrationResult(
currentVersion,
targetVersion,
migratedVersion,
context.GetToVersion(migration),
in context);
return (migratedData, migratedVersion);
}
/// <summary>
/// 校验单步迁移结果与声明目标版本一致,并确保版本严格单调递增且不越过运行时版本。
/// </summary>
/// <typeparam name="TData">迁移数据类型。</typeparam>
/// <typeparam name="TMigration">迁移描述类型。</typeparam>
/// <param name="currentVersion">迁移前版本。</param>
/// <param name="targetVersion">运行时目标版本。</param>
/// <param name="migratedVersion">迁移后实际版本。</param>
/// <param name="declaredTargetVersion">迁移器声明的目标版本。</param>
/// <param name="context">本轮迁移链共享的执行上下文。</param>
/// <exception cref="InvalidOperationException">
/// 声明目标版本不匹配、版本未前进或超出运行时版本时抛出。
/// </exception>
private static void ValidateMigrationResult<TData, TMigration>(
int currentVersion,
int targetVersion,
int migratedVersion,
int declaredTargetVersion,
in MigrationExecutionContext<TData, TMigration> context)
where TData : class
{
if (declaredTargetVersion != migratedVersion)
{
throw new InvalidOperationException(
$"{context.MigrationKind} for {context.SubjectName} declared target version {declaredTargetVersion} " +
$"but returned version {migratedVersion}.");
}
if (migratedVersion <= currentVersion)
{
throw new InvalidOperationException(
$"{context.MigrationKind} for {context.SubjectName} must advance beyond version {currentVersion}.");
}
if (migratedVersion > targetVersion)
{
throw new InvalidOperationException(
$"{context.MigrationKind} for {context.SubjectName} produced version {migratedVersion}, " +
$"which exceeds the current runtime version {targetVersion}.");
}
}
}

View File

@ -0,0 +1,321 @@
# Analyzer Warning Reduction 追踪历史RP-062 至 RP-071
## 范围说明
本归档承接 `RP-062``RP-071` 的 active trace 明细,保留 active 入口在 `RP-072` 压缩前的执行背景、验证里程碑与 PR review 跟进记录。
## superseded by
- [analyzer-warning-reduction-trace.md](../../traces/analyzer-warning-reduction-trace.md)
---
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-071
### 阶段:同步 PR #291 latest-head 对 active todo 的唯一剩余线程
- 触发背景:
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
- 最新抓取结果显示 latest-head open review thread 只剩 `1` 条,且不再指向源码 warning而是指向 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 中已过时的 `.codex` 风险描述
- `.gitignore` 已在当前分支的 `chore(git)` 提交中加入 `.codex`,因此 active todo 若继续保留“当前 worktree 仍存在未跟踪的 `.codex` 目录”会与当前 head 真值冲突
- 主线程实施:
- 重新运行 PR review 抓取脚本,确认 PR `#291` 仍为 `OPEN`,且 latest-head 唯一 open thread 是 CodeRabbit 针对 active todo 的文档同步建议
- 更新 active todo 恢复点为 `RP-071`,将 `.gitignore` 纳入“已提交的低风险批次文件”,并移除已过时的 `.codex` 活跃风险描述
- 在 active todo 中明确保留当前仍未采纳的两条 non-blocking nitpick`VersionedMigrationRunner.cs` 的上下文一致性建议,以及 active trace 归档 RP-062 ~ RP-064 的建议
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#291` latest-head open review threads 为 `1`,唯一路径为 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md:54`
- `dotnet build`
- 结果:成功;`0 Warning(s)``0 Error(s)`;该次为增量 Debug 构建只作为本轮文档同步的完成校验warning 权威基线仍保持 `RP-070` 记录的 `639 Warning(s)`
- 当前结论:
- 当前 PR `#291` 唯一仍成立的 latest-head thread 已被本地文档同步吸收,后续只需在新 head 推送后复核 GitHub 状态
- 剩余 CodeRabbit nitpick 仍是可选整理项,本轮保持 analyzer-warning-reduction 的小写集策略,不把它们混入当前提交
- 下一轮默认先推送并重新抓取 PR review若 open thread 清零,则继续回到 warning 热点选择
## 2026-04-25 — RP-070
### 阶段:按 PR #291 latest-head review 收口仍有效的小批次,并刷新新的仓库根基线
- 触发背景:
- 用户显式要求执行 `$gframework-pr-review`,当前分支对应 PR `#291`
- 抓取结果显示 latest-head 只有 1 条未解决 review thread 指向 `AGENTS.md` 英文标点不一致;同时最新 CodeRabbit review body 还包含 `VersionedMigrationRunner.cs` 参数过多与 `MediatorAdvancedFeaturesTests.cs` 未使用测试基础设施这两条本地仍成立的建议
- MegaLinter 仅报出 `dotnet-format` restore 失败test report 为 `2156 passed / 0 failed`,因此本轮重点改为“只吸收仍有效且低风险的 review 建议”
- 主线程实施:
- 将 `AGENTS.md` 中英文规则段的 `dotnet clean` / `dotnet build` / `dotnet test` 列表标点改为英文逗号,直接消化 latest-head open thread
- 删除 `GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs` 中未被任何测试引用的 `TestLoggingBehavior` 静态类型,移除无收益的可变测试基础设施
- 在 `GFramework.Game/Internal/VersionedMigrationRunner.cs` 内引入私有 `MigrationExecutionContext<TData, TMigration>`,把多处 helper 共享的不变迁移上下文收口为参数对象,并同步补齐新增泛型 helper 的 XML 文档
- 明确拒绝把 `TestLogger` 重复实现与 `YamlConfigLoaderTests.cs` 常量位置这类“可选整理”混入本轮 warning 收敛批次
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#291` latest-head open review threads 为 `1`MegaLinter 仅有 `dotnet-format` restore 失败tests 为 `2156 passed`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`326 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:成功;`149 Warning(s)``0 Error(s)`
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`639 Warning(s)``0 Error(s)`,相较 `RP-069``640` 再下降 `1`
- 当前结论:
- PR #291 当前最有价值的 review follow-up 已被主线程吸收,且没有把“可选整理”误当成必须修复项
- 当前仓库根 warning 基线继续下降到 `639`,说明这轮 review 驱动的小批次仍符合 analyzer warning reduction 主题
- 下一轮可继续围绕 `GFramework.Game``GFramework.Cqrs.Tests` 选择新的单文件低风险热点,或在新 head 推送后重新抓取 PR review 判断是否还有剩余有效线程
## 2026-04-25 — RP-069
### 阶段:继续收口 Cqrs.Tests 双文件集合抽象 warning并刷新新的仓库根基线
- 触发背景:
- `RP-068` 收尾后,当前分支的仓库根基线已降到 `645 Warning(s)`branch diff 仍远低于 `$gframework-batch-boot 50`
- 为保持批次小而连续,主线程继续留在 `GFramework.Cqrs.Tests` 项目内,选取两个不涉及跨文件重构的 `MA0016` 切片
- 接受的委派范围:
- worker `Chandrasekhar`
- 文件:`GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs`
- 目标:在同一文件内收敛 `TestLoggingBehavior.LoggedMessages``SagaData``TestDatabaseRequest` 的集合抽象暴露问题
- 结果:未自行提交;主线程接受其工作树改动并纳入本轮批次
- 主线程实施:
- 本地修改 `GFramework.Cqrs.Tests/Logging/TestLogger.cs`
- 将 `Logs``List<LogEntry>` 收口为 `IReadOnlyList<LogEntry>`,保留私有 `_logs` 作为内部存储
- 与 worker 的 `MediatorAdvancedFeaturesTests.cs` 改动合并后,重新执行 `GFramework.Cqrs.Tests` 与仓库根验证,确认双文件批次的净效果
- 验证里程碑:
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`640 Warning(s)``0 Error(s)`,相较 `RP-068``645` 再下降 `5`
- 当前结论:
- `Cqrs.Tests` 双文件批次已确认有效,并继续压低仓库根 warning 基线
- 当前分支距离 `$gframework-batch-boot 50` 的停止阈值仍有很大空间,可以继续按“主线程小切片 + subagent 并行单文件”推进
- 下一轮可优先回到 `GFramework.Core.Tests` 或继续选择新的 `GFramework.Cqrs.Tests` 单文件热点
## 2026-04-25 — RP-068
### 阶段:吸收并行 subagent 小批次,并继续压低仓库根 warning 基线
- 触发背景:
- `RP-067` 收尾后,当前分支的仓库根基线已降到 `649 Warning(s)`branch diff 仅 `9 files`
- 用户明确允许主线程与 subagent 在不冲突的写集上并行推进,因此本轮继续按 `$gframework-batch-boot 50` 规则拆成 3 个单文件切片
- 接受的委派范围:
- worker `Averroes`
- 文件:`GFramework.Core.Tests/Logging/LogContextTests.cs`
- 目标:收敛 `Push_InAsyncContext_ShouldIsolateAcrossThreads()` 内的 `MA0004`
- 结果:提交 `a7fa70e` `fix(core-tests): 清理 LogContextTests 异步等待 warning`
- worker `Laplace`
- 文件:`GFramework.Core.Tests/Logging/LoggerTests.cs`
- 目标:把 `TestLogger.Logs``List<LogEntry>` 收口为集合抽象以修复 `MA0016`
- 结果:提交 `9f6204d` `fix(core-tests): 收口 LoggerTests 日志集合抽象`
- 主线程实施:
- 本地重构 `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs`
- 将 `RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()` 中的 mock 装配、handler 类型读取、预期集合断言与 metadata lookup verify 拆分为具名 helper
- 在吸收两个 worker 提交后,主线程重新执行直接受影响模块与仓库根验证,确保并行切片合并后的真值一致
- 验证里程碑:
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:成功;`155 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsHandlerRegistrarTests.RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers"`
- 结果:成功;`Passed 1/1`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`645 Warning(s)``0 Error(s)`,相较 `RP-067``649` 再下降 `4`
- 当前结论:
- 并行 3 文件批次已确认有效,且主线程已把 subagent 的负责范围和验证结果收口进 active trace
- 当前分支距离 `$gframework-batch-boot 50` 的停止阈值仍有充足空间,可以继续用“主线程验证 + subagent 并行单文件切片”的节奏推进
- 下一轮可优先回到 `GFramework.Cqrs.Tests``GFramework.Game` 的单文件 `MA0051` / `MA0016` 热点
## 2026-04-25 — RP-067
### 阶段:收口 Game runtime 单文件长方法切片,并继续压低根构建 warning 基线
- 触发背景:
- `RP-066` 收尾后,当前分支已通过 `be26640``YamlConfigLoaderTests.cs` 的 4 个 `MA0051` 落地,仓库根基线降到 `652 Warning(s)`
- 主线程随后切到 `GFramework.Game/Internal/VersionedMigrationRunner.cs`,继续挑选单文件、低风险、可独立验证的 runtime warning 切片
- 主线程实施:
- 将 `MigrateToTargetVersion` 中的运行时版本校验、迁移解析、单步应用与结果一致性校验拆分为具名 helper
- 为新增 helper 补齐 XML 注释,保持该共享迁移执行器的职责边界可读,并避免仅靠机械拆分留下语义不清的私有方法
- 保持外部行为不变,只收敛长方法 warning不扩展到存储或日志相关调用方
- 验证里程碑:
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`649 Warning(s)``0 Error(s)`,相较 `RP-066``652` 再下降 `3`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `VersionedMigrationRunner.cs` 这个 runtime 单文件批次已被主线程收口,并继续压低仓库根 warning 基线
- 本轮只新增 1 个源码唯一文件branch diff 仍显著低于 `$gframework-batch-boot 50` 的主停止阈值
- 下一轮可以继续挑选 `GFramework.Cqrs.Tests``GFramework.Game` 的单文件轻量切片并保持主线程验证、subagent 并行探索的节奏
## 2026-04-25 — RP-066
### 阶段:主线程回收停滞的单文件批次,并继续压低根构建 warning 基线
- 触发背景:
- `RP-065` 收尾后,`fix/analyzer-warning-reduction-batch` 已通过 `6a704f3` 把 AGENTS / active ai-plan 真值修正和 4 文件测试噪音批次提交到分支
- 原先负责 `YamlConfigLoaderTests.cs` 的 worker 长时间无结果,主线程收回该单文件批次以避免继续阻塞
- 主线程实施:
- 关闭停滞 worker直接重构 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`
- 通过提取固定夹具内容、热重载接线 helper 与共享断言,收敛以下 4 个长方法 warning
- `EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks`
- `EnableHotReload_Should_Support_Options_Object`
- `EnableHotReload_Should_Keep_Previous_Table_When_Schema_Change_Makes_Reload_Fail`
- `EnableHotReload_Should_Keep_Previous_State_When_Dependency_Table_Breaks_Cross_Table_Reference`
- 在第一次仓库根重建中命中了两个 `CS0411` 泛型推断错误,主线程随即补上显式类型参数并重新建立 clean/build 基线
- 验证里程碑:
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`652 Warning(s)``0 Error(s)`,相较 `RP-065``656` 再下降 `4`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `YamlConfigLoaderTests.cs` 这 4 个根构建直接确认的 `MA0051` 已被消化
- 当前分支在 `6a704f3` 之后的下一提交只会新增 1 个唯一文件,因此 branch diff 仍明显低于 `$gframework-batch-boot 50` 阈值
- 下一轮可继续选择新的单文件或小写集热点,而不必暂停当前 batch loop
## 2026-04-25 — RP-065
### 阶段:确认 .NET 验证噪音来自沙箱,并把无沙箱直跑写成仓库规则
- 触发背景:
- 用户明确指出“之前很多清理、构建、测试报错像是环境问题,需要申请权限在沙箱外执行”,并要求把该解决方案写入 `AGENTS.md`
- 主线程随后在同一 worktree 中对比了沙箱内与提权后直接 shell 的 `dotnet clean` / `dotnet build`
- 主线程实施:
- 在沙箱内直接运行 `dotnet clean` 时再次复现“Build FAILED but 0 errors”的无诊断噪音
- 申请提权后重新执行同一条 `dotnet clean`,确认命令可正常完成,说明先前 clean 失败并非仓库真值
- 在同一提权上下文直接执行 `dotnet build`,拿到当前仓库根权威基线:`656 Warning(s)``0 Error(s)`
- 关闭正在运行的 warning-reduction worker把工作重心切到仓库治理与 active recovery 文档净化
- 更新 `AGENTS.md`,新增规则:当沙箱内 `dotnet clean` / `dotnet build` / `dotnet test` 产生缺少诊断、权限错误或其他环境噪音时,必须申请沙箱外重跑同一条直接命令,并以该结果为准
- 刷新 active todo/trace把“环境阻塞”从默认恢复入口中降级为历史噪音不再作为当前真值
- 并行工作:
- worker 收敛了 4 个低风险测试噪音文件:
- `GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs`
- `GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs`
- `GFramework.Game.Tests/Config/YamlConfigTextValidatorTests.cs`
- `GFramework.Ecs.Arch.Tests/Ecs/EcsAdvancedTests.cs`
- worker 验证:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- worker 初次结果:成功;随后主线程在同一提权环境复核后确认当前为 `0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj -c Release`
- 主线程复核:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- 本仓库当前在 agent 沙箱内执行 `.NET` 验证时,确实可能出现假失败或缺失诊断
- 当前应把“提权后的直接 `dotnet` 命令输出”视为仓库真值,而不是继续围绕沙箱噪音扩展 workaround 命令形态
- `fix/analyzer-warning-reduction-batch` 当前 `HEAD` 已与 `origin/main` 对齐;新的 `$gframework-batch-boot 50` 轮次从 `0 files / 0 lines` committed diff 开始
- 下一轮低风险热点仍是 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs``4``MA0051`
## 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 编译错误,再暂停在环境阻塞点并准备提交
- 触发背景:
- 用户显式要求先执行 `$gframework-pr-review`,并指出 `AsyncExtensionsTests.cs(126,23)` 当前存在 `CS0029` / `CS1662` 构建错误
- 当前 worktree 仍是 `fix/analyzer-warning-reduction-batch`,因此本 turn 继续沿用 `analyzer-warning-reduction` 的 active recovery 文档
- 主线程实施:
- 运行 PR review 抓取脚本,确认当前分支对应 PR `#288`
- 核对 latest-head unresolved review threads 后,优先修复 `AsyncExtensionsTests.cs``ct => Task.Delay(...).ConfigureAwait(false)` 错误返回 `ConfiguredTaskAwaitable` 的问题
- 顺手收敛多处已被 latest review 点名且本地仍成立的低风险残留:
- 测试中的 `async``await`
- `ValueTask` 断言包装
- `RegistryInitializationHookBaseTests.cs` 的可空返回签名
- `NumericExtensions.cs``StringExtensions.cs``StoreBuilder.cs` 的 Allman 花括号残留
- `StoreSelection.cs``net9.0+` 下切到 `System.Threading.Lock`,同时保留 `net8.0` 兼容分支
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#288` 的 latest-head unresolved AI review threads 共 `9` 个,其中 `AsyncExtensionsTests.cs:126` 为 critical 编译错误
- `DOTNET_CLI_HOME=<dotnet-cli-home> MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 仍读取失效 Windows fallback package folder `<stale-windows-fallback-package-folder>`
- `DOTNET_CLI_HOME=<dotnet-cli-home> MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- `DOTNET_CLI_HOME=<dotnet-cli-home> MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- 当前结论:
- 用户点名的 `AsyncExtensionsTests.cs` 编译错误已在源码层修复
- 本 turn 未能拿到新的可通过 Release build阻塞点已从先前记录的 `MSB4276` 收敛为当前 `obj/*.csproj.nuget.g.props` 中 stale Windows fallback package folder 导致的 `MSB4018`
- 用户随后要求“先不管这个了,先提交吧”,因此本 turn 在记录环境阻塞后先执行提交收口
## 2026-04-25 — RP-062
### 阶段:触达 `$gframework-batch-boot 75` 停止阈值并收口到 `75 files / 2098 lines`
- 触发背景:
- `RP-061` 收尾时分支相对 `origin/main` 仍只有 `48` 个已提交文件,距离本轮 `75 files` 停止条件还有明显空间
- 用户明确允许继续委派 subagent因此主线程继续把低风险机械型写集拆成互不重叠的 test / runtime 小批次
- 本轮主目标不是继续深挖单个高上下文热点,而是用新的低风险文件精确把 branch diff 推到阈值后停止
- 主线程实施:
- 先接受并提交 7 文件 `Core.Tests` 收尾批次为 `03c73a8` `test(core-tests): 收敛测试桩与辅助类型 warning`
- 随后主线程与多个 worker 并行收口以下新增文件:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- 将上述 22 文件批次收口为 `9ce1fa6` `refactor(core): 收敛 Core 扩展与测试的机械 warning`
- 验证里程碑:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 结果:失败;`MSB4276`,默认 SDK resolver 缺少 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,不能作为 `Core.Tests` 的 net8 旁路验证
- `git diff --name-only origin/main...HEAD | wc -l`
- 结果:`75`
- `git diff --numstat origin/main...HEAD`
- 结果:累计 `1083` added、`1015` deleted`2098` changed lines
- 当前结论:
- 本轮 `$gframework-batch-boot 75` 已精确达到主停止条件,默认恢复点应停止在 `9ce1fa6`
- `Core` runtime 的本轮机械型改动已有可通过的最小 Release build 验证
- `Core.Tests` 的继续推进当前首先受 `MSB4276` 环境阻塞影响;下一轮若要继续,应先修复构建环境,再重新建立 warning 基线

View File

@ -6,85 +6,37 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-064`
- 当前阶段:`Phase 64`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-073`
- 当前阶段:`Phase 73`
- 当前焦点:
- `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优先重新抓取 PR #288 的 unresolved 线程并按最新 head 再做一轮收口
- `2026-04-26` 主线程再次按 `$gframework-pr-review` 复核当前分支 PR `#291`,确认 latest-head 仍剩 `2` 条 open review thread均指向 `ai-plan` 文档中的绝对路径记录
- 当前批次同步 active todo/trace 与相关 archive trace把 PR review 输出路径、临时 `dotnet` home 和失效 Windows fallback package folder 改写为仓库安全占位符
- `dotnet clean` + `dotnet build` 的直接仓库根基线仍为 `639 Warning(s)``0 Error(s)`,因此本轮属于文档真值收口,而不是新的 warning 清理批次
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `9964962``2026-04-24T23:05:53+08:00`)。
- 本轮 `Core.Tests` 低风险机械型清理已落地到:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `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`
- `StateMachineSystemTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `NumericExtensions.cs`
- `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`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`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal` 当前结果为 `0 Warning(s)``0 Error(s)`,可作为本轮 runtime 变更的最终最小 Release build 验证。
- `GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental``03c73a8` 提交前的最近一次可信主线程结果为 `198 Warning(s)``0 Error(s)`;该观测值覆盖了 `ArchitectureContextTests``ArchitectureServicesTests``GameContextTests``ResultTests``AsyncTestModel``AsyncTestSystem``ContextAwareEnvironmentExtensionsTests` 的 7 文件批次。
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行;本轮主停止条件已经达到。
- 当前 `origin/main` 基线提交为 `4ad880c``2026-04-25T14:35:38+08:00`)。
- 提权后的直接仓库根验证当前确认为:
- `dotnet clean`
- 结果:成功;此前沙箱内 “Build FAILED but 0 errors” 的 clean 结果不是仓库真值
- `dotnet build`
- 最新结果:成功;`639 Warning(s)``0 Error(s)`
- 当前分支低风险批次文件:
- `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
- `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
- `ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md`
- 当前批次验证结果:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 最新主线程结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
- `dotnet build`
- 最新主线程结果:成功;`639 Warning(s)``0 Error(s)`;与当前权威仓库根基线一致
## 当前风险
- `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应开新批次按模块系统化收敛。
- `GFramework.Core``GFramework.Game``GFramework.Core.Tests``GFramework.Cqrs.Tests` 仍有较大 warning 基线。
- 缓解措施:后续批次继续优先挑低风险、少文件、可独立验证的测试与局部逻辑切片。
- 当前 review 相关真值要等新 head 推送后才能在 GitHub UI 中自动收口。
- 缓解措施:本轮提交后立即重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head thread 与 nitpick 是否消失。
## 活跃文档
@ -94,41 +46,18 @@
- [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-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)
## 验证说明
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 历史结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 历史结果:失败;`MSB4276`,默认 SDK resolver 无法解析 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`,属于当前 WSL / dotnet 10 环境阻塞
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 命中失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018`,原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`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`
- 当前结果:`75`
- `git diff --numstat origin/main...HEAD`
- 当前结果:累计 `1083` added、`1015` deleted`2098` changed lines
- 权威验证结果统一维护在“当前活跃事实”和“当前批次验证结果”。
- 后续若刷新构建或 PR review 真值,只更新上述权威区块,不在本节重复抄录。
## 下一步建议
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 目标或限定到单模块)后再继续
1. 推送包含本轮 absolute-path 脱敏的提交后,重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head open thread 是否已自动收口。
2. 若 PR `#291` 已清零,继续以当前 `639 Warning(s)` 根基线为恢复点,按 `$gframework-batch-boot 50` 规则挑选下一个 1-3 文件的低风险热点。
3. 若 GitHub 仍保留 review 信号,先确认它们是否仍指向新 head再决定是否需要继续清理同主题下的其它历史 `ai-plan` 记录。

View File

@ -1,119 +1,31 @@
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-064
## 2026-04-26 — RP-073
### 阶段:按标准 WSL build 路径复核 PR #288 建议并完成本轮收口
### 阶段:脱敏 analyzer-warning-reduction 文档中的绝对路径记录
- 触发背景:
- 用户指出“在 WSL 里直接执行 `dotnet build` 可以成功”,要求主线程按普通路径重新验证,而不是继续使用带 `MSBuildEnableWorkloadResolver=false``--no-restore`、手工 `TargetFramework` 的 workaround 命令
- 当前任务仍属于 PR #288 review follow-up因此本轮重点改为“区分哪些 AI 建议值得采纳”以及“用真实 WSL build 结果验证”
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
- 最新抓取结果确认 latest-head 还剩 `2` 条 open review thread分别指向 active todo 与 archive trace 中记录的绝对路径
- active trace 当前也保留了同类 `/tmp` 路径记录;虽然这次 review 没直接点名,但继续保留会留下同一类治理缺口
- 主线程实施:
- 重新抓取 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`
- 将 active todo 与 active trace 中的 PR review 输出路径改写为 `--json-output <current-pr-review-json>`
- 将 [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md) 里的临时 `dotnet` home、PR review 输出路径和失效 Windows fallback package folder 改写为仓库安全占位符
- 同步刷新 active todo 中的 review 真值,把当前恢复点更新到 `RP-073`
- 验证里程碑:
- `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)`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
- `dotnet build`
- 结果:成功;`639 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 编译错误,再暂停在环境阻塞点并准备提交
- 触发背景:
- 用户显式要求先执行 `$gframework-pr-review`,并指出 `AsyncExtensionsTests.cs(126,23)` 当前存在 `CS0029` / `CS1662` 构建错误
- 当前 worktree 仍是 `fix/analyzer-warning-reduction-batch`,因此本 turn 继续沿用 `analyzer-warning-reduction` 的 active recovery 文档
- 主线程实施:
- 运行 PR review 抓取脚本,确认当前分支对应 PR `#288`
- 核对 latest-head unresolved review threads 后,优先修复 `AsyncExtensionsTests.cs``ct => Task.Delay(...).ConfigureAwait(false)` 错误返回 `ConfiguredTaskAwaitable` 的问题
- 顺手收敛多处已被 latest review 点名且本地仍成立的低风险残留:
- 测试中的 `async``await`
- `ValueTask` 断言包装
- `RegistryInitializationHookBaseTests.cs` 的可空返回签名
- `NumericExtensions.cs``StringExtensions.cs``StoreBuilder.cs` 的 Allman 花括号残留
- `StoreSelection.cs``net9.0+` 下切到 `System.Threading.Lock`,同时保留 `net8.0` 兼容分支
- 验证里程碑:
- `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 共 `9` 个,其中 `AsyncExtensionsTests.cs:126` 为 critical 编译错误
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 仍读取失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- 当前结论:
- 用户点名的 `AsyncExtensionsTests.cs` 编译错误已在源码层修复
- 本 turn 未能拿到新的可通过 Release build阻塞点已从先前记录的 `MSB4276` 收敛为当前 `obj/*.csproj.nuget.g.props` 中 stale Windows fallback package folder 导致的 `MSB4018`
- 用户随后要求“先不管这个了,先提交吧”,因此本 turn 在记录环境阻塞后先执行提交收口
## 2026-04-25 — RP-062
### 阶段:触达 `$gframework-batch-boot 75` 停止阈值并收口到 `75 files / 2098 lines`
- 触发背景:
- `RP-061` 收尾时分支相对 `origin/main` 仍只有 `48` 个已提交文件,距离本轮 `75 files` 停止条件还有明显空间
- 用户明确允许继续委派 subagent因此主线程继续把低风险机械型写集拆成互不重叠的 test / runtime 小批次
- 本轮主目标不是继续深挖单个高上下文热点,而是用新的低风险文件精确把 branch diff 推到阈值后停止
- 主线程实施:
- 先接受并提交 7 文件 `Core.Tests` 收尾批次为 `03c73a8` `test(core-tests): 收敛测试桩与辅助类型 warning`
- 随后主线程与多个 worker 并行收口以下新增文件:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- 将上述 22 文件批次收口为 `9ce1fa6` `refactor(core): 收敛 Core 扩展与测试的机械 warning`
- 验证里程碑:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 结果:失败;`MSB4276`,默认 SDK resolver 缺少 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,不能作为 `Core.Tests` 的 net8 旁路验证
- `git diff --name-only origin/main...HEAD | wc -l`
- 结果:`75`
- `git diff --numstat origin/main...HEAD`
- 结果:累计 `1083` added、`1015` deleted`2098` changed lines
- 当前结论:
- 本轮 `$gframework-batch-boot 75` 已精确达到主停止条件,默认恢复点应停止在 `9ce1fa6`
- `Core` runtime 的本轮机械型改动已有可通过的最小 Release build 验证
- `Core.Tests` 的继续推进当前首先受 `MSB4276` 环境阻塞影响;下一轮若要继续,应先修复构建环境,再重新建立 warning 基线
- 本轮只吸收当前仍成立的 PR review 文档项,不扩展到新的 warning 清理切片
- 当前仓库根 warning 权威基线仍保持 `639 Warning(s)``0 Error(s)`;本轮目标是让 analyzer-warning-reduction 主题下当前入口不再记录绝对路径
- 下一轮默认先推送本轮同步并重新执行 `$gframework-pr-review`,确认 PR `#291` 的 open thread 是否已自动收口
## 历史归档指针
- 最新 trace 归档:
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- 早期 trace 归档:
- [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)