From 1c87272f6b66e2bce60fab8a8ad11107a5cfeff9 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:53:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(tooling):=20=E8=A1=A5=E5=85=A8PR=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=8A=A5=E5=91=8A=E8=A7=A3=E6=9E=90=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=B9=B6=E5=8F=91=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 gframework-pr-review 脚本以提取 CTRF 测试摘要和失败用例详情 - 修复 SettingsModelTests 在 NET9+ 下错误使用 Monitor 持锁的并发测试语义 - 同步 analyzer-warning-reduction 的 active todo 与 trace 真值 --- .agents/skills/gframework-pr-review/SKILL.md | 1 + .../scripts/fetch_current_pr_review.py | 219 +++++++++++++++--- .../Setting/SettingsModelTests.cs | 43 +++- .../analyzer-warning-reduction-tracking.md | 31 ++- .../analyzer-warning-reduction-trace.md | 39 ++-- 5 files changed, 263 insertions(+), 70 deletions(-) diff --git a/.agents/skills/gframework-pr-review/SKILL.md b/.agents/skills/gframework-pr-review/SKILL.md index 20db5d4b..c8dbcfdb 100644 --- a/.agents/skills/gframework-pr-review/SKILL.md +++ b/.agents/skills/gframework-pr-review/SKILL.md @@ -62,6 +62,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 +- Detailed failed-test rows from GitHub Test Reporter / CTRF comments when the PR comment includes `Name` / `Failure Message` content - 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 diff --git a/.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py b/.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py index 773503a7..05273e02 100644 --- a/.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py +++ b/.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py @@ -257,6 +257,11 @@ def strip_markdown_links(text: str) -> str: return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) +def strip_markdown_images(text: str) -> str: + """Drop Markdown image syntax while keeping surrounding text readable.""" + return re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text) + + def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None: """Extract text between a start marker and the earliest matching end marker.""" start = text.find(start_marker) @@ -486,43 +491,190 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]: return report +def clean_markdown_table_cell(text: str) -> str: + """Normalize a Markdown table cell for structured parsing.""" + cleaned = strip_markdown_images(strip_markdown_links(html.unescape(text))) + cleaned = cleaned.replace("\xa0", " ") + cleaned = cleaned.replace("**", "").replace("*", "").replace("`", "") + return collapse_whitespace(cleaned) + + +def parse_int_from_text(text: str) -> int | None: + """Extract the first integer value from text.""" + match = re.search(r"\d+", text) + return int(match.group(0)) if match else None + + +def parse_duration_from_text(text: str) -> str: + """Extract a duration token from text when present.""" + match = re.search(r"\d+(?:\.\d+)?(?:ms|s|m|h)", text) + if match is not None: + return match.group(0) + + return collapse_whitespace(text) + + +def parse_markdown_table(table_text: str) -> tuple[list[str], list[list[str]]]: + """Parse a Markdown table into header cells and row cells.""" + lines = [line.strip() for line in table_text.splitlines() if line.strip().startswith("|")] + if len(lines) < 2: + return [], [] + + headers = [clean_markdown_table_cell(cell) for cell in lines[0].strip("|").split("|")] + rows: list[list[str]] = [] + for line in lines[2:]: + cells = [clean_markdown_table_cell(cell) for cell in line.strip("|").split("|")] + if cells: + rows.append(cells) + + return headers, rows + + +def extract_markdown_table_after_heading(block: str, heading: str) -> tuple[list[str], list[list[str]]]: + """Extract the first Markdown table that appears after a heading.""" + section = extract_section(block, heading, ["\n### ", "\n#### ", "\n
", "\n", "\n"]) + if section is None: + return [], [] + + table_match = re.search(r"(\|.*\|\n\|[-| :]+\|\n(?:\|.*\|\n?)*)", section, re.S) + if table_match is None: + return [], [] + + return parse_markdown_table(table_match.group(1)) + + +def normalize_stat_header(header: str) -> str: + """Normalize a human-readable stats header into a stable machine key.""" + ascii_only = re.sub(r"[^A-Za-z]+", "", header).lower() + aliases = { + "tests": "tests", + "passed": "passed", + "failed": "failed", + "skipped": "skipped", + "pending": "pending", + "other": "other", + "flaky": "flaky", + "duration": "duration", + } + return aliases.get(ascii_only, ascii_only) + + +def parse_stats_table(headers: list[str], rows: list[list[str]]) -> dict[str, Any]: + """Convert a parsed Markdown stats table into the report stats shape.""" + if not headers or not rows: + return {} + + first_row = rows[0] + stats: dict[str, Any] = {} + for header, value in zip(headers, first_row): + key = normalize_stat_header(header) + if not key: + continue + + if key == "duration": + stats[key] = parse_duration_from_text(value) + continue + + parsed_value = parse_int_from_text(value) + if parsed_value is not None: + stats[key] = parsed_value + + return stats + + +def normalize_failure_message(text: str) -> str: + """Normalize a failed-test message while preserving the meaningful lines.""" + cleaned = html.unescape(text) + cleaned = re.sub(r"(?i)", "\n", cleaned) + cleaned = re.sub(r"", "\n", cleaned) + cleaned = re.sub(r"<[^>]+>", " ", cleaned) + lines = [collapse_whitespace(line) for line in cleaned.splitlines()] + meaningful_lines = [line for line in lines if line] + return "\n".join(meaningful_lines) + + +def parse_failed_test_summary_list(block: str) -> list[str]: + """Parse the compact failed-tests summary list from CTRF details blocks.""" + failed_tests_section = re.search( + r"
\s*Failed Tests.*?(?P.*?)
", + block, + re.S, + ) + if failed_tests_section is None: + return [] + + summary_body = strip_markdown_links(strip_markdown_images(html.unescape(failed_tests_section.group("body")))) + failed_tests: list[str] = [] + for raw_line in summary_body.splitlines(): + line = collapse_whitespace(raw_line) + if not line: + continue + + if "arrow-right" in raw_line: + parts = [part.strip() for part in line.split("arrow-right") if part.strip()] + candidate = parts[-1] if parts else line + elif ">" in line: + candidate = line.split(">")[-1].strip() + else: + candidate = line + + if candidate: + failed_tests.append(candidate) + + return failed_tests + + +def parse_failed_test_details(block: str) -> list[dict[str, str]]: + """Parse the detailed failed-test HTML table from GitHub Test Reporter comments.""" + details: list[dict[str, str]] = [] + table_section = re.search( + r"### ❌ \*\*Some tests failed!\*\*.*?
(?P.*?)", + block, + re.S, + ) + if table_section is None: + return details + + for name_cell, message_cell in re.findall(r"\s*\s*\s*", table_section.group("body"), re.S): + name = collapse_whitespace(strip_tags(html.unescape(name_cell))).lstrip("❌").strip() + failure_message = normalize_failure_message(message_cell) + if name: + details.append( + { + "name": name, + "failure_message": failure_message, + } + ) + + return details + + def parse_test_report(block: str) -> dict[str, Any]: """Parse a CTRF or GitHub test-reporter comment block.""" report: dict[str, Any] = { "raw": block.strip(), "stats": {}, "failed_tests": [], + "failed_test_details": [], "has_failed_tests": False, } - summary_row_match = re.search( - r"\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|" - r"\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?([^\|]+?)\*?\*?\s*\|", - block, - ) - if summary_row_match is not None: - report["stats"] = { - "tests": int(summary_row_match.group(1)), - "passed": int(summary_row_match.group(2)), - "failed": int(summary_row_match.group(3)), - "skipped": int(summary_row_match.group(4)), - "other": int(summary_row_match.group(5)), - "flaky": int(summary_row_match.group(6)), - "duration": summary_row_match.group(7).strip(), - } + summary_headers, summary_rows = extract_markdown_table_after_heading(block, "### Summary") + report["stats"] = parse_stats_table(summary_headers, summary_rows) - failed_tests_section = extract_section( - block, - "### Failed Tests", - ["### Slowest Tests", "### Insights", "", "[Github Test Reporter]"], - ) - if failed_tests_section: - lines = [line.strip("- ").strip() for line in failed_tests_section.splitlines()[1:] if line.strip()] - report["failed_tests"] = lines - report["has_failed_tests"] = True - elif "No failed tests in this run." in block or "All tests passed!" in block: - report["failed_tests"] = [] - report["has_failed_tests"] = False + if not report["stats"]: + build_headers, build_rows = extract_markdown_table_after_heading(block, "### build-and-test:") + report["stats"] = parse_stats_table(build_headers, build_rows) + + failed_test_details = parse_failed_test_details(block) + failed_test_names = parse_failed_test_summary_list(block) + if not failed_test_names and failed_test_details: + failed_test_names = [detail["name"] for detail in failed_test_details] + + report["failed_tests"] = failed_test_names + report["failed_test_details"] = failed_test_details + failed_count = int(report["stats"].get("failed", 0) or 0) + report["has_failed_tests"] = bool(failed_test_names or failed_test_details or failed_count > 0) return report @@ -1103,8 +1255,17 @@ def format_text( 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: {truncate_text(failed_test, max_description_length)}") + failed_test_details = report.get("failed_test_details", []) + if failed_test_details: + for failed_test_detail in failed_test_details: + lines.append(f" Failed test: {truncate_text(failed_test_detail['name'], max_description_length)}") + lines.append( + " Failure: " + f"{truncate_text(failed_test_detail['failure_message'].replace(chr(10), ' | '), max_description_length)}" + ) + else: + 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") diff --git a/GFramework.Game.Tests/Setting/SettingsModelTests.cs b/GFramework.Game.Tests/Setting/SettingsModelTests.cs index 8f7fa047..d5111c6c 100644 --- a/GFramework.Game.Tests/Setting/SettingsModelTests.cs +++ b/GFramework.Game.Tests/Setting/SettingsModelTests.cs @@ -137,12 +137,10 @@ public sealed class SettingsModelTests var migrationMapLock = lockField!.GetValue(model); Assert.That(migrationMapLock, Is.Not.Null); - Task initializeTask; - Task registerTask; - lock (migrationMapLock!) + var tasks = WithSynchronizationLockHeld(migrationMapLock!, () => { - initializeTask = Task.Run(() => model.InitializeAsync()); - registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3())); + var initializeTask = Task.Run(() => model.InitializeAsync()); + var registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3())); Thread.Sleep(50); @@ -151,7 +149,11 @@ public sealed class SettingsModelTests Assert.That(initializeTask.IsCompleted, Is.False); Assert.That(registerTask.IsCompleted, Is.False); }); - } + + return (initializeTask, registerTask); + }); + + var (initializeTask, registerTask) = tasks; await Task.WhenAll(initializeTask, registerTask); @@ -171,6 +173,35 @@ public sealed class SettingsModelTests }); } + /// + /// 以与被测代码相同的同步原语持有反射获取到的锁对象,避免在 .NET 9+ 上把 + /// 退化成 语义,导致并发测试误判。 + /// + /// 通过反射读取到的私有锁字段。 + /// 持锁代码返回的结果类型。 + /// 持锁期间执行的断言与并发调度逻辑。 + /// 持锁代码的返回值。 + private static TResult WithSynchronizationLockHeld(object syncRoot, Func action) + { + ArgumentNullException.ThrowIfNull(syncRoot); + ArgumentNullException.ThrowIfNull(action); + +#if NET9_0_OR_GREATER + if (syncRoot is System.Threading.Lock typedLock) + { + using (typedLock.EnterScope()) + { + return action(); + } + } +#endif + + lock (syncRoot) + { + return action(); + } + } + private sealed class TestSettingsData : ISettingsData { public string Value { get; set; } = "default"; diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 0c499cc0..81e20c6d 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,12 +6,12 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-079` -- 当前阶段:`Phase 79` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-080` +- 当前阶段:`Phase 80` - 当前焦点: - - `2026-04-27` 已按 PR `#295` 的 latest-head review 收口本地仍成立的问题:`AsyncExtensionsTests` 的 `ArgumentException.ParamName` 契约与 active `ai-plan` 文档过长问题 - - 当前轮次已重新确认 `origin/main` 基线与 `HEAD` 同为 `617e0bf`;当前已提交 `HEAD` 的 stop metric 仍为 `30 / 50` files、`642` changed lines,本轮 PR review 同步尚未提交入该指标 - - 当前剩余 warning 主要集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review 收口的低风险边界 + - `2026-04-27` 已补齐 `$gframework-pr-review` 对 GitHub Test Reporter / CTRF 测试摘要与失败用例详情的提取,当前文本输出可以直接显示失败测试名和 failure message 摘要 + - `SettingsModelTests.RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 已按 `System.Threading.Lock` 的真实语义修正测试实现,并完成针对性 Release 测试验证 + - 当前剩余 warning 热点仍集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review / test follow-up 的低风险边界 ## 当前活跃事实 @@ -19,12 +19,10 @@ - 当前 PR review 真值: - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output ` - 最新结果:成功;当前分支对应 PR 为 `#295` - - latest-head review 在本轮修复前共有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 与 active `trace` - - 本轮已在本地收口上述两类问题,待提交后可重新执行 `$gframework-pr-review` 确认线程自动收口 -- 提权后的直接验证当前确认为: - - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` - - 最新结果:成功;`126 Warning(s)`、`0 Error(s)` - - 当前仍可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008` + - 当前测试报告输出已能显示 `Summary` 统计、失败测试名称,以及 `Name / Failure Message` 表格中的关键信息 +- 当前直接验证结果: + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"` + - 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1` - 当前分支 stop-condition 指标: - `git diff --name-only refs/remotes/origin/main...HEAD | wc -l` - 最新结果:`30` @@ -33,6 +31,7 @@ - 当前批次摘要: - 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397` - 当前批次的已完成 slice 明细已迁移到归档,active todo 仅保留恢复真值 + - 本轮新增内容为 PR review 工具链补强与单个失败测试修正,不扩展 warning reduction 的热点清理边界 - 当前建议保留到下一波次的候选: - `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`(单点可修,但文件本身同时承载其他高耦合 warning) - 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数) @@ -43,8 +42,8 @@ - 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。 - `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。 - 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。 -- 当前 PR open thread 的自动收口仍依赖新提交进入远端 PR head。 - - 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,仅以最新 head review 为准。 +- 当前 PR open thread 与 CI 失败信号仍依赖新提交进入远端 PR head 才能复核。 + - 缓解措施:本轮提交后重新执行 `$gframework-pr-review`,同时确认 review thread 与 failed test signal 是否一起收口。 ## 活跃文档 @@ -69,6 +68,6 @@ ## 下一步建议 -1. 提交本轮 PR review 修复与 `ai-plan` 精简同步,再重跑 `$gframework-pr-review` 验证 PR `#295` 的 open thread 是否随新 head 收口。 -2. 若后续继续推进 warning reduction,建议另开下一波次,优先明确是否接受 `YamlConfigLoader.cs` 热点触碰,或是否要专门做测试项目 `MA0048` 拆分波次。 -3. 默认在当前恢复点停下,因为继续推进已不再符合本轮“PR review 收口 + 少文件同步”的边界。 +1. 提交本轮 `$gframework-pr-review` 解析增强、`SettingsModelTests` 修复与 `ai-plan` 同步。 +2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 open thread 是否已更新为新 head 真值。 +3. 若后续继续推进 warning reduction,建议另开下一波次处理 `YamlConfigLoader.cs` 热点或测试项目 `MA0048` 拆分波次。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 671ee903..81202a73 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,37 +1,38 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-27 — RP-079 +## 2026-04-27 — RP-080 -### 阶段:按 PR `#295` latest-head review 收口当前仍成立的 open thread +### 阶段:补强 `$gframework-pr-review` 的测试报告提取并修复 `SettingsModelTests` 失败用例 - 触发背景: - - 重新执行 `$gframework-pr-review` 后,确认当前分支对应的最新公开 PR 为 `#295`,而不是旧 trace 中记录的 `#291` - - latest-head review 仍有 `2` 条 open thread,分别指向 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 的参数名契约问题,以及 active trace 过长问题 + - 用户补充了 GitHub Test Reporter / CTRF 的完整 PR 评论,指出现有 `$gframework-pr-review` 输出虽然能显示 `failed=1`,但没有抓到失败测试名称与详细报错 + - PR 评论中的真实失败用例为 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache`,报错集中在测试通过反射持锁后两个任务提前完成 - 主线程实施: - - 修正 `ThrowShouldNotRetry` 中 `ArgumentException` 的 `ParamName` 传递逻辑,并补充断言锁定 `nameof(taskFactory)` 契约 - - 将 active trace 精简为单一恢复入口,并把 `RP073` 到 `RP078` 的详细过程迁入归档 - - 同步压缩 active todo,只保留当前恢复点真值与归档指针 + - 扩展 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 `parse_test_report`,增加 CTRF `Summary` 统计、紧凑 failed-tests 列表、以及 `Name` / `Failure Message` HTML 表格的提取 + - 更新 `gframework-pr-review` skill 文档的输出预期,明确脚本现在应提取 GitHub Test Reporter / CTRF 的失败详情 + - 修正 `GFramework.Game.Tests/Setting/SettingsModelTests.cs`,让反射取得的 `_migrationMapLock` 在 `NET9_0_OR_GREATER` 下通过 `System.Threading.Lock.EnterScope()` 持锁,而不是退化成 `Monitor` 语义 - 验证里程碑: - - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output ` - - 结果:成功;确认 PR `#295` latest-head review 在本轮修复前共有 `2` 条 open thread - - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` - - 结果:成功;`126 Warning(s)`、`0 Error(s)` - - 当前可见的既有 warning ID:`CS8766`、`CS8625`、`CS8602`、`CS8618`、`MA0048`、`MA0002`、`MA0008` + - `python3 -c "... parse_test_report(...)"`(基于 `/tmp/current-pr-review.json` 的现有原始评论) + - 结果:成功;已能解析 `tests=2156`、`passed=2155`、`failed=1`、`duration=35.3s`,并抓到 `RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache` 的 failure message + - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-v2.json` + - 结果:成功;文本输出已直接显示失败测试名与报错摘要 + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"` + - 结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1` - 当前结论: - - 当前仍成立的 latest-head review 问题已经在本地收口,下一步应以新提交刷新 PR head 再复查 - - active `ai-plan` 入口现已回到“当前真值 + 归档指针”的体量,符合仓库对恢复文档的要求 + - `$gframework-pr-review` 现在已经能把 PR 测试评论里的关键失败用例信息直接提出来,不再只停留在 `failed=1` + - `SettingsModelTests` 当前失败是测试锁语义与生产代码不一致导致的误判,已在本地复现并修正通过 ## 活跃风险 -- PR review thread 是否自动关闭仍取决于新提交是否进入远端 PR head。 - - 缓解措施:提交后重新执行 `$gframework-pr-review`,只以新的 latest-head 结果为准。 +- PR 上的 latest-head review thread 与测试报告仍需要等新提交进入远端后再复核。 + - 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只以新的 latest-head 和 test report 为准。 - `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与 `MA0048` 拆分仍是下一波次的高耦合候选。 - - 缓解措施:保持本轮边界只处理 PR review 收口,不顺手扩展 warning reduction 范围。 + - 缓解措施:保持本轮边界只处理 PR review 工具链与失败测试,不顺手扩展 warning reduction 范围。 ## 下一步 -1. 完成本轮验证并提交。 -2. 重新执行 `$gframework-pr-review`,确认 PR `#295` latest-head 是否还有 unresolved thread。 +1. 完成本轮提交。 +2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 failed test detail 与 unresolved thread 是否已刷新。 ## 历史归档指针
(.*?)(.*?)