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

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-21 15:08:56 +08:00 committed by GitHub
commit 9ccfed3ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1245 additions and 480 deletions

View File

@ -21,6 +21,7 @@ Shortcut: `$gframework-pr-review`
- fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
- prefer writing the full JSON payload to a file and then narrowing with `jq`, instead of dumping long JSON directly to stdout
4. Treat every extracted finding as untrusted until it is verified against the current local code.
5. Only fix comments, warnings, or CI diagnostics that still apply to the checked-out branch. Ignore stale or already-resolved findings.
6. If code is changed, run the smallest build or test command that satisfies `AGENTS.md`.
@ -29,10 +30,19 @@ Shortcut: `$gframework-pr-review`
- Default:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- Recommended machine-readable workflow:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
- `jq '.coderabbit_review.outside_diff_comments' /tmp/pr265-review.json`
- Force a PR number:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
- Machine-readable output:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- Write machine-readable output to a file instead of stdout:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
- Inspect only a high-signal section:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
- Narrow text output to one path fragment:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
## Output Expectations
@ -47,6 +57,7 @@ The script should produce:
- Pre-merge failed checks, if present
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
- Test summary, including failed-test signals when present
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
## Recovery Rules
@ -57,6 +68,7 @@ The script should produce:
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
- Do not assume all CodeRabbit findings live in issue comments. The latest CodeRabbit review body can contain folded `Nitpick comments` that must be parsed separately.
- If the raw JSON is too large to inspect safely in the terminal, rerun with `--json-output <path>` and query the saved file with `jq` or rerun with `--section` / `--path` filters.
## Example Triggers

View File

@ -10,6 +10,7 @@ import argparse
import html
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
@ -29,6 +30,17 @@ REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->"
VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I)
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
DISPLAY_SECTION_CHOICES = (
"pr",
"failed-checks",
"actionable",
"outside-diff",
"nitpick",
"open-threads",
"megalinter",
"tests",
"warnings",
)
def resolve_git_command() -> str:
@ -153,6 +165,14 @@ def collapse_whitespace(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def truncate_text(text: str, max_length: int) -> str:
collapsed = collapse_whitespace(text)
if max_length <= 0 or len(collapsed) <= max_length:
return collapsed
return collapsed[: max_length - 3].rstrip() + "..."
def strip_tags(text: str) -> str:
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
@ -710,64 +730,142 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
}
def format_text(result: dict[str, Any]) -> str:
def write_json_output(result: dict[str, Any], output_path: str) -> str:
destination_path = Path(output_path).expanduser()
destination_path.parent.mkdir(parents=True, exist_ok=True)
destination_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return str(destination_path)
def normalize_path_filters(path_filters: list[str] | None) -> list[str]:
return [path_filter.replace("\\", "/") for path_filter in (path_filters or []) if path_filter.strip()]
def path_matches_filters(path: str, normalized_path_filters: list[str]) -> bool:
if not normalized_path_filters:
return True
normalized_path = path.replace("\\", "/")
return any(path_filter in normalized_path for path_filter in normalized_path_filters)
def filter_comments_by_path(
comments: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [comment for comment in comments if path_matches_filters(str(comment.get("path") or ""), normalized_path_filters)]
def filter_threads_by_path(
threads: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [thread for thread in threads if path_matches_filters(str(thread.get("path") or ""), normalized_path_filters)]
def format_text(
result: dict[str, Any],
*,
sections: list[str] | None = None,
path_filters: list[str] | None = None,
max_description_length: int = 400,
json_output_path: str | None = None,
) -> str:
lines: list[str] = []
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
normalized_path_filters = normalize_path_filters(path_filters)
pr = result["pull_request"]
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
if "pr" in selected_sections:
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
failed_checks = result["coderabbit_summary"].get("failed_checks", [])
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {check['explanation']}")
lines.append(f" Resolution: {check['resolution']}")
if "failed-checks" in selected_sections:
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {truncate_text(check['explanation'], max_description_length)}")
lines.append(f" Resolution: {truncate_text(check['resolution'], max_description_length)}")
coderabbit_comments = result.get("coderabbit_comments", {})
review_feedback = result.get("coderabbit_review", {})
comments = coderabbit_comments.get("comments", [])
visible_comments = filter_comments_by_path(comments, normalized_path_filters)
actionable_count = review_feedback.get("actionable_count") or coderabbit_comments.get("count") or len(comments)
lines.append("")
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
for comment in comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
if "actionable" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit actionable comments: {actionable_count} total"
+ (
f", {len(visible_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if actionable_count and not visible_comments:
lines.append(" Details: no actionable comments matched the current path filter.")
elif actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
outside_diff_comments = review_feedback.get("outside_diff_comments", [])
visible_outside_diff_comments = filter_comments_by_path(outside_diff_comments, normalized_path_filters)
outside_diff_count = review_feedback.get("outside_diff_count") or len(outside_diff_comments)
lines.append("")
lines.append(f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed")
for comment in outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "outside-diff" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed"
+ (
f", {len(visible_outside_diff_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if outside_diff_comments and not visible_outside_diff_comments:
lines.append(" Details: no outside-diff comments matched the current path filter.")
nitpick_comments = review_feedback.get("nitpick_comments", [])
visible_nitpick_comments = filter_comments_by_path(nitpick_comments, normalized_path_filters)
nitpick_count = review_feedback.get("nitpick_count") or len(nitpick_comments)
lines.append("")
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
for comment in nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "nitpick" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed"
+ (
f", {len(visible_nitpick_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if nitpick_comments and not visible_nitpick_comments:
lines.append(" Details: no nitpick comments matched the current path filter.")
latest_commit_review = result.get("latest_commit_review", {})
latest_commit = latest_commit_review.get("latest_commit", {})
latest_review = latest_commit_review.get("latest_review", {})
open_threads = latest_commit_review.get("open_threads", [])
if latest_commit:
visible_open_threads = filter_threads_by_path(open_threads, normalized_path_filters)
if latest_commit and "open-threads" in selected_sections:
lines.append("")
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
if latest_review:
@ -780,23 +878,32 @@ def format_text(result: dict[str, Any]) -> str:
lines.append(
"Latest commit review threads: "
f"{len(latest_commit_review.get('threads', []))} total, {len(open_threads)} open"
+ (
f", {len(visible_open_threads)} shown after path filter"
if normalized_path_filters
else ""
)
)
for thread in open_threads:
for thread in visible_open_threads:
root_comment = thread["root_comment"]
latest_comment = thread["latest_comment"]
lines.append(f"- {thread['path']}:{thread['line']}")
lines.append(f" Root by {root_comment['user']}: {collapse_whitespace(root_comment['body'])}")
lines.append(f" Root by {root_comment['user']}: {truncate_text(root_comment['body'], max_description_length)}")
if latest_comment["id"] != root_comment["id"]:
lines.append(f" Latest by {latest_comment['user']}: {collapse_whitespace(latest_comment['body'])}")
lines.append(
f" Latest by {latest_comment['user']}: {truncate_text(latest_comment['body'], max_description_length)}"
)
if contains_visible_addressed_commit_text(root_comment["body"]) or contains_visible_addressed_commit_text(
latest_comment["body"]
):
lines.append(
" Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches."
)
if open_threads and not visible_open_threads:
lines.append(" Details: no open threads matched the current path filter.")
megalinter_report = result.get("megalinter_report", {})
if megalinter_report:
if megalinter_report and "megalinter" in selected_sections:
lines.append("")
lines.append(
"MegaLinter: "
@ -818,32 +925,37 @@ def format_text(result: dict[str, Any]) -> str:
for issue in megalinter_report.get("detailed_issues", []):
lines.append(f"- Detailed issue: {issue['summary']}")
lines.append(f" {collapse_whitespace(issue['details'])}")
lines.append(f" {truncate_text(issue['details'], max_description_length)}")
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
lines.append(f"- Report {index}: no structured test stats parsed")
if "tests" in selected_sections:
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
lines.append(f"- Report {index}: no structured test stats parsed")
if report["has_failed_tests"]:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {failed_test}")
else:
lines.append(" Failed tests: none reported")
if report["has_failed_tests"]:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
else:
lines.append(" Failed tests: none reported")
if result["parse_warnings"]:
if result["parse_warnings"] and "warnings" in selected_sections:
lines.append("")
lines.append("Warnings:")
for warning in result["parse_warnings"]:
lines.append(f"- {warning}")
lines.append(f"- {truncate_text(warning, max_description_length)}")
if json_output_path:
lines.append("")
lines.append(f"Full JSON written to: {json_output_path}")
return "\n".join(lines)
@ -853,6 +965,27 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--branch", help="Override the current branch name.")
parser.add_argument("--pr", type=int, help="Fetch a specific PR number instead of resolving from branch.")
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument(
"--json-output",
help="Write the full JSON result to a file. When used with --format text, stdout stays concise and points to the file.",
)
parser.add_argument(
"--section",
action="append",
choices=DISPLAY_SECTION_CHOICES,
help="Limit text output to specific sections. Can be passed multiple times.",
)
parser.add_argument(
"--path",
action="append",
help="Only show comments and review threads whose path contains this fragment. Can be passed multiple times.",
)
parser.add_argument(
"--max-description-length",
type=int,
default=400,
help="Truncate long text bodies in text output to this many characters.",
)
return parser.parse_args()
@ -866,12 +999,27 @@ def main() -> None:
pr_number = resolve_pr_number(branch)
result = build_result(pr_number, branch)
json_output_path: str | None = None
if args.json_output:
json_output_path = write_json_output(result, args.json_output)
if args.format == "json":
if json_output_path:
print(json_output_path)
return
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(format_text(result))
print(
format_text(
result,
sections=args.section,
path_filters=args.path,
max_description_length=args.max_description_length,
json_output_path=json_output_path,
)
)
if __name__ == "__main__":

View File

@ -345,6 +345,20 @@ public class CoroutineSchedulerTests
Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(10));
}
/// <summary>
/// 验证调度器在零初始容量下会在首次启动协程时自动扩容,而不是写入越界。
/// </summary>
[Test]
public void Run_Should_Grow_From_Zero_Initial_Capacity()
{
var scheduler = new CoroutineScheduler(new TestTimeSource(), initialCapacity: 0);
var handle = scheduler.Run(CreateYieldingCoroutine(new WaitOneFrame()));
Assert.That(handle.IsValid, Is.True);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
}
/// <summary>
/// 验证协程调度器应该使用提供的时间源
/// </summary>
@ -563,4 +577,4 @@ public class TestTimeSource : ITimeSource
DeltaTime = 0.1;
CurrentTime += DeltaTime;
}
}
}

View File

@ -124,6 +124,24 @@ public class EventTests
Assert.That(values, Does.Contain(10));
}
/// <summary>
/// 测试单参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventT_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
Action<int> handler = _ => { };
_eventInt.Register(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(1));
_eventInt.UnRegister(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
}
/// <summary>
/// 测试带两个泛型参数的事件注册功能是否正确添加处理器
/// </summary>
@ -161,4 +179,22 @@ public class EventTests
_eventIntString.Trigger(2, "b");
Assert.That(count, Is.EqualTo(1));
}
}
/// <summary>
/// 测试双参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
Action<int, string> handler = (_, _) => { };
_eventIntString.Register(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(1));
_eventIntString.UnRegister(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
}
}

View File

@ -1,3 +1,6 @@
using System.Collections;
using System.Reflection;
namespace GFramework.Core.Tests.StateManagement;
/// <summary>
@ -384,6 +387,32 @@ public class StoreTests
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
}
/// <summary>
/// 测试当 dispatch 作用域在快照阶段抛出异常时Store 不会残留“正在分发”标记而锁死后续调用。
/// </summary>
[Test]
public void Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws()
{
var store = new Store<CounterState>(
new CounterState(0, "Player"),
actionMatchingMode: StoreActionMatchingMode.IncludeAssignableTypes);
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
var reducers = GetReducersDictionary(store);
var throwingActionType = new ThrowingAssignableType(typeof(IncrementAction), "simulated reducer snapshot failure");
reducers.Add(throwingActionType, CreateEmptyReducerRegistrationList(reducers));
Assert.That(
() => store.Dispatch(new IncrementAction(1)),
Throws.InvalidOperationException.With.Message.EqualTo("simulated reducer snapshot failure"));
reducers.Remove(throwingActionType);
Assert.That(() => store.Dispatch(new IncrementAction(1)), Throws.Nothing);
Assert.That(store.State.Count, Is.EqualTo(1));
}
/// <summary>
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
/// </summary>
@ -681,6 +710,32 @@ public class StoreTests
return store;
}
/// <summary>
/// 读取 Store 内部 reducer 字典,以便在异常安全回归测试中注入受控的异常源。
/// </summary>
/// <param name="store">要读取的 Store 实例。</param>
/// <returns>Store 当前持有的 reducer 字典引用。</returns>
private static IDictionary GetReducersDictionary(Store<CounterState> store)
{
var reducersField = typeof(Store<CounterState>).GetField("_reducers", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Unable to locate Store reducer dictionary field.");
return (IDictionary)(reducersField.GetValue(store)
?? throw new InvalidOperationException("Store reducer dictionary should not be null."));
}
/// <summary>
/// 创建与 Store 私有 reducer 注册列表兼容的空列表实例。
/// </summary>
/// <param name="reducers">现有 reducer 字典,用于推断私有列表元素类型。</param>
/// <returns>可写入私有 reducer 字典的空列表。</returns>
private static object CreateEmptyReducerRegistrationList(IDictionary reducers)
{
var valueType = reducers.GetType().GenericTypeArguments[1];
return Activator.CreateInstance(valueType)
?? throw new InvalidOperationException("Unable to create an empty reducer registration list.");
}
/// <summary>
/// 用于测试的计数器状态。
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
@ -876,4 +931,20 @@ public class StoreTests
next();
}
}
}
/// <summary>
/// 用于在回归测试中稳定模拟 <see cref="Type.IsAssignableFrom(Type)"/> 失败的代理类型。
/// </summary>
private sealed class ThrowingAssignableType(Type delegatingType, string message) : TypeDelegator(delegatingType)
{
/// <summary>
/// 在 Store 创建 reducer 快照时抛出受控异常,验证 dispatch 作用域能够正确回滚。
/// </summary>
/// <param name="typeInfo">待比较的 action 运行时类型。</param>
/// <returns>此实现不会正常返回。</returns>
public override bool IsAssignableFrom(Type? typeInfo)
{
throw new InvalidOperationException(message);
}
}
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
@ -26,3 +27,54 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync();
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且无返回值的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法。
/// </summary>
/// <returns>表示异步操作的任务。</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作的任务。</returns>
protected abstract Task OnExecuteAsync(TInput input);
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且返回结果的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行结果类型。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法。
/// </summary>
/// <returns>表示异步操作且包含结果的任务。</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作且包含结果的任务。</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,29 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理无返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法
/// </summary>
/// <returns>表示异步操作的任务</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理有返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法
/// </summary>
/// <returns>表示异步操作且包含结果的任务</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作且包含结果的任务</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,5 +1,7 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
@ -20,4 +22,55 @@ public abstract class AbstractCommand : ContextAwareBase, ICommand
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
protected abstract void OnExecute();
}
}
/// <summary>
/// 抽象命令类,实现 <see cref="ICommand" /> 接口,为需要命令输入的具体命令提供基础架构支持。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="ICommand" /> 接口的 <c>Execute</c> 方法。
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
protected abstract void OnExecute(TInput input);
}
/// <summary>
/// 带返回值的抽象命令类,为需要输入和返回值的命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="GFramework.Core.Abstractions.Command.ICommand{TResult}" /> 接口的
/// <c>Execute</c> 方法。
/// </summary>
/// <returns>命令执行后的结果。</returns>
TResult GFramework.Core.Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
/// <returns>命令执行后的结果。</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -1,28 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象命令类,实现 ICommand 接口,为具体命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
protected abstract void OnExecute(TInput input);
}

View File

@ -1,31 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 带返回值的抽象命令类,实现 ICommand{TResult} 接口,为需要返回结果的命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法
/// </summary>
/// <returns>命令执行后的结果</returns>
TResult Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
/// <returns>命令执行后的结果</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -16,7 +16,7 @@ namespace GFramework.Core.Coroutine;
/// </remarks>
/// <param name="timeSource">缩放时间源,提供调度器默认推进所使用的时间数据。</param>
/// <param name="instanceId">协程实例编号,用于生成带宿主前缀的句柄。</param>
/// <param name="initialCapacity">调度器初始槽位容量。</param>
/// <param name="initialCapacity">调度器初始槽位容量;允许为 0此时首次启动协程会按需自动扩容。</param>
/// <param name="enableStatistics">是否启用协程统计功能。</param>
/// <param name="realtimeTimeSource">
/// 非缩放时间源。
@ -211,58 +211,10 @@ public sealed class CoroutineScheduler(
return default;
}
if (_nextSlot >= _slots.Length)
{
Expand();
}
var handle = new CoroutineHandle(instanceId);
var slotIndex = _nextSlot++;
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
if (cancellationToken.CanBeCanceled)
{
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
_slots[slotIndex] = slot;
_metadata[handle] = new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
var slotIndex = AllocateSlotIndex();
var slot = CreateRunningSlot(handle, coroutine, priority, cancellationToken);
RegisterStartedCoroutine(handle, slotIndex, slot, priority, tag, group);
Prewarm(slotIndex);
UpdateStatisticsSnapshot();
@ -662,70 +614,14 @@ public sealed class CoroutineScheduler(
CoroutineCompletionStatus completionStatus,
Exception? exception = null)
{
var slot = _slots[slotIndex];
if (slot == null)
if (!TryGetFinalizableCoroutine(slotIndex, out var slot, out var handle))
{
return;
}
var handle = slot.Handle;
if (!handle.IsValid)
{
return;
}
if (_metadata.TryGetValue(handle, out var meta))
{
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
UpdateCompletionMetadata(handle, completionStatus);
ReleaseCompletedCoroutine(slotIndex, slot, handle);
CompleteCoroutineLifecycle(handle, completionStatus);
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
}
@ -799,6 +695,139 @@ public sealed class CoroutineScheduler(
}
}
/// <summary>
/// 为新协程分配槽位索引,并在需要时扩容槽位数组。
/// </summary>
/// <returns>可写入的新槽位索引。</returns>
private int AllocateSlotIndex()
{
if (_nextSlot >= _slots.Length)
{
Expand();
}
return _nextSlot++;
}
/// <summary>
/// 创建处于运行态的协程槽位,并在需要时挂接跨线程取消回调。
/// </summary>
/// <param name="handle">新协程句柄。</param>
/// <param name="coroutine">协程枚举器。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
/// <returns>已初始化的协程槽位。</returns>
private CoroutineSlot CreateRunningSlot(
CoroutineHandle handle,
IEnumerator<IYieldInstruction> coroutine,
CoroutinePriority priority,
CancellationToken cancellationToken)
{
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
RegisterCancellationCallback(slot, handle, cancellationToken);
return slot;
}
/// <summary>
/// 为支持取消的协程注册待终止排队回调。
/// </summary>
/// <param name="slot">目标协程槽位。</param>
/// <param name="handle">协程句柄。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
private void RegisterCancellationCallback(
CoroutineSlot slot,
CoroutineHandle handle,
CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
{
return;
}
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
/// <summary>
/// 将新协程写入调度器的槽位、元数据、标签分组和完成状态跟踪结构。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已初始化的协程槽位。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
private void RegisterStartedCoroutine(
CoroutineHandle handle,
int slotIndex,
CoroutineSlot slot,
CoroutinePriority priority,
string? tag,
string? group)
{
_slots[slotIndex] = slot;
_metadata[handle] = CreateCoroutineMetadata(slotIndex, priority, tag, group);
ResetCompletionTracking(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
}
/// <summary>
/// 创建新协程的初始元数据。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
/// <returns>与新槽位对应的元数据对象。</returns>
private CoroutineMetadata CreateCoroutineMetadata(
int slotIndex,
CoroutinePriority priority,
string? tag,
string? group)
{
return new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
}
/// <summary>
/// 重置协程完成跟踪,使复用句柄不会携带上一轮完成结果。
/// </summary>
/// <param name="handle">协程句柄。</param>
private void ResetCompletionTracking(CoroutineHandle handle)
{
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
}
/// <summary>
/// 释放单个槽位持有的资源。
/// </summary>
@ -824,6 +853,125 @@ public sealed class CoroutineScheduler(
slot.Waiting = null;
}
/// <summary>
/// 读取可被完成处理的协程槽位与句柄。
/// 当槽位已空或句柄已失效时,说明该协程已经被其他路径清理,无需重复执行结束逻辑。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">若成功则返回槽位。</param>
/// <param name="handle">若成功则返回句柄。</param>
/// <returns>当存在可完成的协程时返回 <see langword="true" />。</returns>
private bool TryGetFinalizableCoroutine(int slotIndex, out CoroutineSlot slot, out CoroutineHandle handle)
{
var candidate = _slots[slotIndex];
if (candidate == null)
{
slot = null!;
handle = default;
return false;
}
handle = candidate.Handle;
if (!handle.IsValid)
{
slot = null!;
return false;
}
slot = candidate;
return true;
}
/// <summary>
/// 根据最终状态更新协程元数据与统计信息。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void UpdateCompletionMetadata(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
if (!_metadata.TryGetValue(handle, out var meta))
{
return;
}
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
ApplyCompletionMetadata(meta, completionStatus);
}
/// <summary>
/// 将最终结果映射到元数据状态和统计记录。
/// </summary>
/// <param name="meta">协程元数据。</param>
/// <param name="completionStatus">最终结果。</param>
private void ApplyCompletionMetadata(CoroutineMetadata meta, CoroutineCompletionStatus completionStatus)
{
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
/// <summary>
/// 释放已结束协程占用的槽位和索引结构。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已结束的协程槽位。</param>
/// <param name="handle">协程句柄。</param>
private void ReleaseCompletedCoroutine(int slotIndex, CoroutineSlot slot, CoroutineHandle handle)
{
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
}
/// <summary>
/// 完成协程的等待者唤醒、任务结果和完成历史记录。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void CompleteCoroutineLifecycle(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
}
/// <summary>
/// 唤醒所有等待目标协程完成的协程。
/// </summary>
@ -888,7 +1036,9 @@ public sealed class CoroutineScheduler(
/// </summary>
private void Expand()
{
Array.Resize(ref _slots, _slots.Length * 2);
// 允许构造器以 0 容量启动,用于极简场景或测试;首次分配时至少扩到 1避免后续写槽位越界。
var expandedLength = Math.Max(1, _slots.Length * 2);
Array.Resize(ref _slots, expandedLength);
}
/// <summary>

View File

@ -3,24 +3,24 @@
namespace GFramework.Core.Events;
/// <summary>
/// 泛型事件类,支持一个泛型参数 T 的事件注册、注销与触发。
/// 实现了 IEvent 接口以提供统一的事件操作接口。
/// 泛型事件类,支持一个泛型参数 <typeparamref name="T" /> 的事件注册、注销与触发。
/// 实现了 <see cref="IEvent" /> 接口以提供统一的事件操作接口。
/// </summary>
/// <typeparam name="T">事件回调函数的第一个参数类型。</typeparam>
public class Event<T> : IEvent
{
/// <summary>
/// 存储已注册的事件处理委托。
/// 默认为空操作no-op委托避免 null 检查
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T>? _mOnEvent = _ => { };
private Action<T>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
@ -35,7 +35,7 @@ public class Event<T> : IEvent
/// 注册一个事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T> onEvent)
{
_mOnEvent += onEvent;
@ -52,7 +52,7 @@ public class Event<T> : IEvent
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" />
/// </summary>
/// <param name="t">传递给事件处理程序的参数。</param>
public void Trigger(T t)
@ -61,9 +61,9 @@ public class Event<T> : IEvent
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
@ -71,30 +71,30 @@ public class Event<T> : IEvent
}
/// <summary>
/// 支持两个泛型参数 T 和 TK 的事件类。
/// 支持两个泛型参数 <typeparamref name="T" /><typeparamref name="TK" /> 的事件类。
/// 提供事件注册、注销和触发功能。
/// </summary>
/// <typeparam name="T">第一个参数类型。</typeparam>
/// <typeparam name="Tk">第二个参数类型。</typeparam>
public class Event<T, Tk> : IEvent
/// <typeparam name="TK">第二个参数类型。</typeparam>
public class Event<T, TK> : IEvent
{
/// <summary>
/// 存储已注册的双参数事件处理委托。
/// 默认为空操作no-op委托
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T, Tk>? _mOnEvent = (_, _) => { };
private Action<T, TK>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
void Action(T _, Tk __)
void Action(T _, TK __)
{
onEvent();
}
@ -104,8 +104,8 @@ public class Event<T, Tk> : IEvent
/// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, Tk> onEvent)
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, TK> onEvent)
{
_mOnEvent += onEvent;
return new DefaultUnRegister(() => UnRegister(onEvent));
@ -115,27 +115,27 @@ public class Event<T, Tk> : IEvent
/// 取消指定的双参数事件监听器。
/// </summary>
/// <param name="onEvent">需要被注销的事件处理方法。</param>
public void UnRegister(Action<T, Tk> onEvent)
public void UnRegister(Action<T, TK> onEvent)
{
_mOnEvent -= onEvent;
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t 和 k。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" /><paramref name="k" />
/// </summary>
/// <param name="t">第一个参数。</param>
/// <param name="k">第二个参数。</param>
public void Trigger(T t, Tk k)
public void Trigger(T t, TK k)
{
_mOnEvent?.Invoke(t, k);
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
}
}
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -24,4 +25,31 @@ public abstract class AbstractAsyncQuery<TResult> : ContextAwareBase, IAsyncQuer
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync();
}
}
/// <summary>
/// 抽象异步查询基类,为需要输入参数的异步查询提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">查询输入类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(TInput input)
: ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作。
/// </summary>
/// <returns>返回查询结果的异步任务。</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>返回查询结果的异步任务。</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,33 +0,0 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象异步查询基类用于处理输入类型为TInput、结果类型为TResult的异步查询操作
/// </summary>
/// <typeparam name="TInput">查询输入类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="input">查询输入参数</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(
TInput input
) : ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -7,7 +8,7 @@ namespace GFramework.Core.Query;
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
public abstract class AbstractQuery<TResult> : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
{
/// <summary>
@ -25,4 +26,31 @@ public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
/// </summary>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo();
}
}
/// <summary>
/// 抽象查询类,为需要输入参数的同步查询提供基础实现。
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果的类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作。
/// </summary>
/// <returns>查询结果,类型为 <typeparamref name="TResult" />。</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>查询结果。</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作
/// </summary>
/// <returns>查询结果类型为TResult</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -212,10 +212,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
ArgumentNullException.ThrowIfNull(action);
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
IStoreMiddleware<TState>[] middlewaresSnapshot = Array.Empty<IStoreMiddleware<TState>>();
IStoreReducerAdapter[] reducersSnapshot = Array.Empty<IStoreReducerAdapter>();
IEqualityComparer<TState> stateComparerSnapshot = _stateComparer;
StoreDispatchContext<TState>? context = null;
TState notificationState = default!;
var hasNotification = false;
var enteredDispatchScope = false;
@ -224,49 +220,25 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
try
{
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
enteredDispatchScope = true;
context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
}
var context = EnterDispatchScope(
action,
out var middlewaresSnapshot,
out var reducersSnapshot,
out var stateComparerSnapshot);
enteredDispatchScope = true;
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
return;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
hasNotification = listenersSnapshot.Length > 0;
}
hasNotification = TryCommitDispatchResult(context, out listenersSnapshot, out notificationState);
}
finally
{
if (enteredDispatchScope)
{
lock (_lock)
{
_isDispatching = false;
}
ExitDispatchScope();
}
}
}
@ -831,6 +803,99 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
}
/// <summary>
/// 进入一次新的 dispatch 作用域,并在状态锁内抓取本次执行所需的上下文快照。
/// 该方法只做最短路径的共享状态访问,把 middleware/reducer 的实际执行留到锁外完成。
/// </summary>
/// <typeparam name="TAction">action 的具体类型。</typeparam>
/// <param name="action">本次分发的 action。</param>
/// <param name="middlewaresSnapshot">返回本次 dispatch 使用的中间件快照。</param>
/// <param name="reducersSnapshot">返回本次 dispatch 使用的 reducer 快照。</param>
/// <param name="stateComparerSnapshot">返回本次 dispatch 使用的状态比较器快照。</param>
/// <returns>已初始化的 dispatch 上下文。</returns>
private StoreDispatchContext<TState> EnterDispatchScope<TAction>(
TAction action,
out IStoreMiddleware<TState>[] middlewaresSnapshot,
out IStoreReducerAdapter[] reducersSnapshot,
out IEqualityComparer<TState> stateComparerSnapshot)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before entering a dispatch scope.");
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
try
{
var context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
return context;
}
catch
{
// 进入 dispatch 标记早于快照构建;如果这里抛异常,必须同步回滚标记,避免后续调用被永久判定为嵌套分发。
_isDispatching = false;
throw;
}
}
}
/// <summary>
/// 在 dispatch 管线执行完成后提交诊断信息和状态变更。
/// 状态与订阅集合的更新统一在该阶段完成,从而保证 dispatch 与 time-travel 共享同一提交流程。
/// </summary>
/// <param name="context">刚完成 middleware/reducer 管线的 dispatch 上下文。</param>
/// <param name="listenersSnapshot">若需要立即通知,则返回锁外回放的监听器快照。</param>
/// <param name="notificationState">若需要立即通知,则返回要通知的状态。</param>
/// <returns>本次 dispatch 是否需要在锁外执行监听器通知。</returns>
private bool TryCommitDispatchResult(
StoreDispatchContext<TState> context,
out Action<TState>[] listenersSnapshot,
out TState notificationState)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before committing a dispatch result.");
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
listenersSnapshot = Array.Empty<Action<TState>>();
notificationState = default!;
return false;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
return listenersSnapshot.Length > 0;
}
}
/// <summary>
/// 退出当前 dispatch 作用域,允许后续 dispatch 或历史控制继续进入。
/// </summary>
private void ExitDispatchScope()
{
lock (_lock)
{
_isDispatching = false;
}
}
/// <summary>
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
/// </summary>
@ -949,20 +1014,65 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
return CreateExactReducerSnapshot(actionType);
}
return CreateAssignableReducerSnapshot(actionType);
}
/// <summary>
/// 为精确类型匹配模式创建 reducer 快照。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>精确匹配到的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateExactReducerSnapshot(Type actionType)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
}
/// <summary>
/// 为多态匹配模式创建 reducer 快照。
/// 该路径会收集所有可赋值的注册桶,并按“精确类型 -> 基类距离 -> 接口 -> 注册顺序”的稳定规则排序。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>多态模式下的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateAssignableReducerSnapshot(Type actionType)
{
var matches = CollectReducerMatches(actionType);
if (matches is null || matches.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
matches.Sort(CompareReducerMatch);
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
}
return snapshot;
}
/// <summary>
/// 收集当前 action 类型可命中的 reducer 注册,并附带稳定排序所需的匹配元数据。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>匹配结果列表;若没有任何匹配则返回 <see langword="null"/>。</returns>
private List<ReducerMatch>? CollectReducerMatches(Type actionType)
{
List<ReducerMatch>? matches = null;
foreach (var reducerBucket in _reducers)
@ -984,35 +1094,30 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
if (matches is null || matches.Count == 0)
return matches;
}
/// <summary>
/// 比较两个 reducer 匹配结果的优先级,保证多态匹配下的执行顺序稳定可预测。
/// </summary>
/// <param name="left">左侧匹配结果。</param>
/// <param name="right">右侧匹配结果。</param>
/// <returns>排序比较结果。</returns>
private static int CompareReducerMatch(ReducerMatch left, ReducerMatch right)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return Array.Empty<IStoreReducerAdapter>();
return categoryComparison;
}
matches.Sort(static (left, right) =>
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return categoryComparison;
}
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
return distanceComparison;
}
return left.Sequence.CompareTo(right.Sequence);
});
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
return distanceComparison;
}
return snapshot;
return left.Sequence.CompareTo(right.Sequence);
}
/// <summary>
@ -1412,4 +1517,4 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
/// </summary>
public TState PendingState { get; set; } = default!;
}
}
}

View File

@ -7,22 +7,26 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
- 当前阶段:`Phase 5`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-012`
- 当前阶段:`Phase 12`
- 当前焦点:
- 已完成 `GFramework.Core/Pause/PauseStackManager.cs``MA0051` 收口:将 `DestroyAsync``Pop` 拆分为锁内状态迁移、
栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
- 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
- 下一轮若继续推进,优先在 `CoroutineScheduler``Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
`PauseStackManager`
- 当前 PR review workflow 已补强到支持 JSON 落盘与按 section/path 收窄输出;下一轮恢复到 `MA0046` 主批次
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
`GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
delegate 形状和少量公共集合抽象接口问题
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 当前 `PauseStackManager``Store``CoroutineScheduler``GFramework.Core``MA0048`
文件/类型命名冲突已从 active 入口移除;主题内剩余 warning 主要集中在 `MA0046` delegate 形状、
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
## 当前活跃事实
@ -32,16 +36,33 @@
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- `RP-007` 已在不改公共 API 的前提下完成 `CoroutineScheduler` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证
调度、取消与完成状态语义未回归
- `RP-008` 将后续策略从“单文件 warning 切片”切换为“按类型批处理 + 文件数上限控制”,并允许在非冲突前提下使用
不同模型的 subagent 并行处理
- `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core`
`net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command``Query``Event` 路径未回归
- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险:
`CoroutineScheduler``initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留
`_isDispatching = true` 的锁死问题
- `RP-011` 根据补充复核继续收口 PR #265 的 outside-diff comment修复 `Event<T>` / `Event<T, TK>` 默认 no-op
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 结构性重构风险:剩余 `GFramework.Core``MA0051``MA0048` 可能要求较大的文件拆分或类型重命名
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
- 公共契约兼容风险:剩余 `MA0046` / `MA0016` 若直接改公开委托或集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
## 活跃文档
@ -65,11 +86,46 @@
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- `RP-007` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- `RP-008` 的策略基线:
- 当前 `GFramework.Core` 剩余 warning 分布:`MA0048=8``MA0046=6``MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 后续批处理规则:优先按类型推进;若当轮主类型数量不足,可顺手吸收其他低冲突类型,不限定于 `MA0015``MA0077`
- `RP-009` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- `RP-010` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-011` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)``Event.cs` 的 listener count 修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-012` 的定向验证结果:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避技能目录只读导致的 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`
`MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
2. 下一轮优先以 `MA0046` 为主批次启动,先从 `Architecture*``CoroutineScheduler` 的低风险 delegate 形状修正中选一个切入点
3. 若 `MA0046` 的文件 ownership 可以清晰切分,允许使用不同模型的 subagent 并行处理互不冲突的目录或类型簇
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,198 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-012
### 阶段PR review workflow 输出收窄增强RP-012
- 背景:上一轮虽然脚本已经能解析 `outside_diff_comments`,但直接把超长 JSON 打到终端时仍可能因为输出截断而漏看高价值 review 信号
- 本轮对 `gframework-pr-review` 做了工作流级增强,而不是继续依赖 shell 重定向技巧:
- 为 `fetch_current_pr_review.py` 增加 `--json-output <path>`,允许把完整 JSON 稳定写入文件
- 增加 `--section`,可只输出 `outside-diff``open-threads``megalinter` 等高信号文本摘要
- 增加 `--path`,允许把文本输出收窄到特定文件或路径片段
- 增加 `--max-description-length`,避免超长 comment/body 在 text 模式下刷屏
- 当 text 模式搭配 `--json-output`stdout 保持精简,并显式提示完整 JSON 文件路径
- 同步更新 `SKILL.md`
- 将“先落盘,再用 `jq``--section` / `--path` 缩小范围”写成推荐机器工作流
- 补充按 section 和按路径聚焦的示例命令
- 预期收益:
- 不再要求操作者肉眼阅读整份长 JSON
- outside-diff、nitpick 和 open thread 都能成为一等可过滤输出
- 即使终端输出有 token/长度上限,完整结果仍可通过文件稳定回查
- 定向验证命令:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;新增 CLI 选项均已出现在帮助输出中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- 下一步建议:
- 之后执行 `$gframework-pr-review` 时,默认优先使用 `--json-output`
- 在 review 跟进阶段,先看 `outside-diff``open-threads``megalinter` 三个 section再决定是否需要打开完整 JSON
## 2026-04-21 — RP-011
### 阶段PR #265 outside-diff follow-up 补收口RP-011
- 用户补充指出 CodeRabbit 在 `Some comments are outside the diff` 中还有 `GFramework.Core/Events/Event.cs` 的 minor finding
默认 no-op 委托会被 `GetInvocationList()` 计入,导致 `GetListenerCount()` 在无监听器和单监听器场景分别返回 `1``2`
- 本地复核确认该问题仍成立:
- `Event<T>` 当前字段初始化为 `_ => { }`
- `Event<T, TK>` 当前字段初始化为 `(_, _) => { }`
- 两个 `Trigger(...)` 实现本身已是 null-safe因此无需依赖占位委托规避空引用
- 实施最小修复:
- 移除两个事件字段的 no-op 初始委托,改为以 `null` 表示“无监听器”
- 保持 `Register` / `UnRegister` / `Trigger` 的公开 API 和调用方式不变
- 在 `EventTests` 中新增单参数与双参数 `GetListenerCount()` 回归测试,覆盖初始值、注册后和注销后的计数语义
- 过程说明:
- 这条不是 skill 设计遗漏;`gframework-pr-review` 的目标本来就包含 latest review body 和 outside-diff 信号
- 上一轮是我在处理时漏看了这条 outside-diff item且终端里展示的超长 JSON 输出被截断,未单独把 `Event.cs` 项再抽出来复核
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续 PR #265 follow-up只接受当前本地仍成立的剩余 outside-diff 或 unresolved review 项
- 若没有新的有效 review 点,再恢复到 `MA0046` 主批次
## 2026-04-21 — RP-010
### 阶段PR #265 follow-up 收口RP-010
- 使用 `gframework-pr-review` 抓取当前分支 PR #265 的 latest head review threads、CodeRabbit review body、MegaLinter 摘要与 CTRF
测试结果;确认最新 unresolved thread 只剩 `CoroutineScheduler` 零容量扩容边界
- 本地复核后确认两处仍成立的风险:
- `CoroutineScheduler.Expand()``_slots.Length == 0` 时会把容量从 `0` 扩到 `0`,首次 `Run` 写槽位会越界
- `Store.EnterDispatchScope()``_isDispatching = true` 之后、快照构建完成之前若抛异常,会留下永久的嵌套分发误判
- 实施最小修复:
- 将 `Expand()` 调整为 `Math.Max(1, _slots.Length * 2)`,保持已有倍增策略,只补上零容量边界
- 为 `EnterDispatchScope()` 增加快照阶段的异常回滚,确保 `_isDispatching` 与实际 dispatch 生命周期保持一致
- 新增回归测试覆盖零容量启动路径,以及 dispatch 快照阶段抛错后的可恢复性
- 当前 PR 信号复核结论:
- CTRF最新评论显示 `2135 passed / 0 failed`
- MegaLinter唯一告警仍是 CI 中 `dotnet-format` restore 失败,未发现新的本地代码格式问题
- 旧 review body 中提到的 `Store` 异常安全问题虽未表现为最新 open thread但在本地代码中仍可成立因此一并收口
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续本主题,恢复到 `MA0046` 主批次,不再停留在当前 PR follow-up
- 若 PR review 还出现新线程,继续遵守“只修复当前本地仍成立的问题”的策略
## 2026-04-21 — RP-009
### 阶段:`MA0048` 批次收口RP-009
- 依据 `RP-008` 的批处理策略,本轮继续从 `GFramework.Core``MA0048` 启动,但不采用重命名公共类型的高风险做法;
改为把同名不同泛型 arity 的家族收拢到与类型名一致的单文件中
- 具体调整:
- 将 `AbstractCommand<TInput>``AbstractCommand<TInput, TResult>` 合并进 `AbstractCommand.cs`
- 将 `AbstractAsyncCommand<TInput>``AbstractAsyncCommand<TInput, TResult>` 合并进 `AbstractAsyncCommand.cs`
- 将 `AbstractQuery<TInput, TResult>` 合并进 `AbstractQuery.cs`
- 将 `AbstractAsyncQuery<TInput, TResult>` 合并进 `AbstractAsyncQuery.cs`
- 将泛型 `Event<T>` / `Event<T, TK>``EasyEventGeneric.cs` 迁移到 `Event.cs`
- 首次构建暴露出合并后的 `ICommand<TResult>` / `IQuery<TResult>` 命名空间歧义;随后改用
`GFramework.Core.Abstractions.*` 的限定名完成最小修正,没有引入行为改动
- 定向验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)``MA0048` 已从当前 `GFramework.Core` `net8.0` warnings-only 基线中清空
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- 当前建议的下一批次顺序更新为:
- 第一优先级:`MA0046`
- 第二优先级:`MA0016`
- 顺手吸收:`MA0015``MA0077`
- 单独评估:`MA0002`
## 2026-04-21 — RP-008
### 阶段批处理策略切换RP-008
- 根据当前 `GFramework.Core` warnings-only build 的剩余分布,后续不再默认沿用“单文件、单 warning family”的切片节奏
改为按 warning 类型和数量优先级批量推进
- 当前数量基线:
- `MA0048 = 8`
- `MA0046 = 6`
- `MA0016 = 5`
- `MA0002 = 2`
- `MA0015 = 1`
- `MA0077 = 1`
- 新的批处理规则:
- 先按类型选择主批次,而不是按单文件选切入点
- 若主批次数量不够,则允许顺手并入其他低冲突类型;`MA0015``MA0077` 只是当前明显的低数量尾项示例,不是限定范围
- 单次 `boot` 的工作树改动规模控制在约 `100` 个文件以内,避免 recovery context 和 review 面同时膨胀
- 当 warning 类型或目录边界清晰且写集不冲突时,允许使用不同模型的 subagent 并行处理,但必须先定义独占 ownership
- 当前建议的下一批次顺序:
- 第一优先级:`MA0048`
- 第二优先级:`MA0046`
- 顺手吸收:其他低冲突类型,当前可见示例包括 `MA0015``MA0077`
- 单独评估:`MA0016``MA0002`
- 本轮仅更新 recovery strategy不改生产代码验证继续沿用当前基线构建
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)`
## 2026-04-21 — RP-007
### 阶段CoroutineScheduler `MA0051` 收口RP-007
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/Coroutine/CoroutineScheduler.cs`,因为剩余两个 `MA0051` 都集中在协程启动与完成清理路径,且已有
`CoroutineSchedulerTests``CoroutineSchedulerAdvancedTests` 覆盖句柄创建、取消、完成状态、标签分组和等待语义
- 将 `Run` 拆分为:
- `AllocateSlotIndex`
- `CreateRunningSlot`
- `RegisterCancellationCallback`
- `RegisterStartedCoroutine`
- `CreateCoroutineMetadata`
- `ResetCompletionTracking`
- 将 `FinalizeCoroutine` 拆分为:
- `TryGetFinalizableCoroutine`
- `UpdateCompletionMetadata`
- `ApplyCompletionMetadata`
- `ReleaseCompletedCoroutine`
- `CompleteCoroutineLifecycle`
- 保持取消回调只做跨线程入队、`Prewarm` 时机、统计记录文本、`RemoveTag` / `RemoveGroup` / `WakeWaiters` 顺序以及
`OnCoroutineFinished` 的同步触发时机不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- 当前 `MA0051` 主线已经在本主题下完成;下一步若继续,应先重新评估剩余 `MA0048``MA0046``MA0002``MA0016`
收敛价值与改动风险,再决定是否开启下一轮 warning family
## 2026-04-21 — RP-006
### 阶段Store `MA0051` 收口RP-006
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/StateManagement/Store.cs`,因为该文件的两个 `MA0051` 都集中在 dispatch / reducer snapshot 逻辑,
且已有 `StoreTests` 覆盖 dispatch、batch、history 和多态 reducer 匹配语义
- 在正式验证前先处理 WSL 环境噪音:当前 worktree 的 `GFramework.Core/obj/project.assets.json` 是 Windows 侧 restore
产物,`--no-restore` 构建会继续引用宿主 Windows fallback package folder本轮先执行一次 Linux 侧
`dotnet restore GFramework.Core/GFramework.Core.csproj -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> --ignore-failed-sources -nologo`
刷新资产文件,再继续 warnings-only build
- 将 `Dispatch` 拆分为:
- `EnterDispatchScope`
- `TryCommitDispatchResult`
- `ExitDispatchScope`
- 将 `CreateReducerSnapshotCore` 拆分为:
- `CreateExactReducerSnapshot`
- `CreateAssignableReducerSnapshot`
- `CollectReducerMatches`
- `CompareReducerMatch`
- 保持 `_dispatchGate -> _lock` 的锁顺序、middleware 锁外执行、批处理通知折叠以及“精确类型 -> 基类 -> 接口 ->
注册顺序”的 reducer 稳定排序语义不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- 下一步保持同一节奏:只在 `CoroutineScheduler.cs``Run` / `FinalizeCoroutine` 两个 `MA0051` 中继续,不与其他
warning 家族混做
## 2026-04-21 — RP-005
### 阶段PauseStackManager `MA0051` 收口RP-005