From 97b619c0b25a7720ffd3bde71cb2a061bf81a3de Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 19 Apr 2026 09:59:36 +0800
Subject: [PATCH] =?UTF-8?q?fix(codex):=20=E4=BC=98=E5=8C=96=20FPR=20?=
=?UTF-8?q?=E7=9A=84=20API=20=E4=BC=98=E5=85=88=E5=AE=A1=E6=9F=A5=E6=B5=81?=
=?UTF-8?q?=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 FPR 脚本的 Git 解析、--pr 控制流与 branch 到 PR 的 API 解析路径
- 优化 CodeRabbit summary、CTRF 测试结果与 latest head review threads 的 API-first 提取逻辑
- 更新 skill 文档、agent prompt 与 ai-plan 记录,明确最新提交后 review threads 的优先级
---
.codex/skills/gframework-pr-review/SKILL.md | 20 +-
.../gframework-pr-review/agents/openai.yaml | 2 +-
.../scripts/fetch_current_pr_review.py | 390 +++++++++++++++---
.../todos/cqrs-rewrite-migration-tracking.md | 35 +-
.../traces/cqrs-rewrite-migration-trace.md | 21 +-
5 files changed, 381 insertions(+), 87 deletions(-)
diff --git a/.codex/skills/gframework-pr-review/SKILL.md b/.codex/skills/gframework-pr-review/SKILL.md
index cb73e173..d9805f17 100644
--- a/.codex/skills/gframework-pr-review/SKILL.md
+++ b/.codex/skills/gframework-pr-review/SKILL.md
@@ -14,10 +14,11 @@ Shortcut: `$gframework-pr-review`
1. Read `AGENTS.md` before deciding how to validate or fix anything.
2. Resolve the current branch with Windows Git from WSL, following the repository worktree rule.
3. Run `scripts/fetch_current_pr_review.py` to:
- - locate the PR for the current branch
- - fetch the PR conversation page
- - extract `Summary by CodeRabbit`
- - extract `Actionable comments posted`
+ - locate the PR for the current branch through the GitHub PR API
+ - fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
+ - extract `Summary by CodeRabbit` and CTRF test reports from issue comments
+ - 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 and test-report signals such as `Failed Tests` or `No failed tests in this run`
4. Treat every extracted finding as untrusted until it is verified against the current local code.
5. Only fix comments that still apply to the checked-out branch. Ignore stale or already-resolved findings.
@@ -37,17 +38,20 @@ Shortcut: `$gframework-pr-review`
The script should produce:
- PR metadata: number, title, state, branch, URL
-- CodeRabbit summary block
-- Parsed actionable comments grouped by file
+- CodeRabbit summary block from issue comments when available
+- Parsed latest head-review threads, with unresolved threads clearly separated
+- Latest head commit review metadata and review threads
+- Unresolved latest-commit review threads after reply-thread folding
- Pre-merge failed checks, if present
- Test summary, including failed-test signals when present
-- Parse warnings when the page structure changes or a section is missing
+- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
## Recovery Rules
- If the current branch has no matching public PR, report that clearly instead of guessing.
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
-- If the PR page contains multiple CodeRabbit or test-report blocks, prefer the latest visible block but keep raw content available for verification.
+- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
+- 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.
## Example Triggers
diff --git a/.codex/skills/gframework-pr-review/agents/openai.yaml b/.codex/skills/gframework-pr-review/agents/openai.yaml
index 8a501dff..e82522e7 100644
--- a/.codex/skills/gframework-pr-review/agents/openai.yaml
+++ b/.codex/skills/gframework-pr-review/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
display_name: "GFramework PR Review"
short_description: "Inspect the current PR and CodeRabbit findings"
- default_prompt: "Use $gframework-pr-review to inspect the current branch PR, extract CodeRabbit comments, and summarize failed checks or failed tests."
+ default_prompt: "Use $gframework-pr-review to inspect the current branch PR through the GitHub API, prioritize unresolved review threads on the latest head commit, and summarize failed checks or failed tests."
diff --git a/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py b/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py
index 859160d2..d5152f1d 100644
--- a/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py
+++ b/.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py
@@ -9,7 +9,9 @@ from __future__ import annotations
import argparse
import html
import json
+import os
import re
+import shutil
import subprocess
import sys
import urllib.parse
@@ -18,7 +20,55 @@ from typing import Any
OWNER = "GeWuYou"
REPO = "GFramework"
-WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
+DEFAULT_WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
+GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
+USER_AGENT = "codex-gframework-pr-review"
+CODERABBIT_LOGIN = "coderabbitai[bot]"
+REVIEW_COMMENT_ADDRESSED_MARKER = ""
+DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
+REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
+
+
+def resolve_git_command() -> str:
+ candidates = [
+ os.environ.get(GIT_ENVIRONMENT_KEY),
+ DEFAULT_WINDOWS_GIT,
+ "git.exe",
+ "git",
+ ]
+
+ for candidate in candidates:
+ if not candidate:
+ continue
+
+ if os.path.isabs(candidate):
+ if os.path.exists(candidate):
+ return candidate
+ continue
+
+ resolved_candidate = shutil.which(candidate)
+ if resolved_candidate:
+ return resolved_candidate
+
+ raise RuntimeError(f"No usable git executable found. Set {GIT_ENVIRONMENT_KEY} to override it.")
+
+
+def resolve_request_timeout_seconds() -> int:
+ configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
+ if not configured_timeout:
+ return DEFAULT_REQUEST_TIMEOUT_SECONDS
+
+ try:
+ parsed_timeout = int(configured_timeout)
+ except ValueError as error:
+ raise RuntimeError(
+ f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be an integer number of seconds."
+ ) from error
+
+ if parsed_timeout <= 0:
+ raise RuntimeError(f"{REQUEST_TIMEOUT_ENVIRONMENT_KEY} must be greater than zero.")
+
+ return parsed_timeout
def run_command(args: list[str]) -> str:
@@ -30,38 +80,71 @@ def run_command(args: list[str]) -> str:
def get_current_branch() -> str:
- return run_command([WINDOWS_GIT, "rev-parse", "--abbrev-ref", "HEAD"])
+ return run_command([resolve_git_command(), "rev-parse", "--abbrev-ref", "HEAD"])
-def fetch_text(url: str) -> str:
+def open_url(url: str, accept: str) -> tuple[str, Any]:
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
- with opener.open(url, timeout=30) as response:
- return response.read().decode("utf-8", "replace")
+ request = urllib.request.Request(url, headers={"Accept": accept, "User-Agent": USER_AGENT})
+ with opener.open(request, timeout=resolve_request_timeout_seconds()) as response:
+ return response.read().decode("utf-8", "replace"), response.headers
+
+
+def fetch_json(url: str) -> tuple[Any, Any]:
+ text, headers = open_url(url, accept="application/vnd.github+json")
+ return json.loads(text), headers
+
+
+def extract_next_link(headers: Any) -> str | None:
+ link_header = headers.get("Link")
+ if not link_header:
+ return None
+
+ match = re.search(r'<([^>]+)>;\s*rel="next"', link_header)
+ return match.group(1) if match else None
+
+
+def fetch_paged_json(url: str) -> list[dict[str, Any]]:
+ items: list[dict[str, Any]] = []
+ next_url: str | None = url
+ while next_url:
+ payload, headers = fetch_json(next_url)
+ if not isinstance(payload, list):
+ raise RuntimeError(f"Expected list payload from GitHub API, got {type(payload).__name__}.")
+
+ items.extend(payload)
+ next_url = extract_next_link(headers)
+
+ return items
+
+
+def fetch_pull_request_metadata(pr_number: int) -> dict[str, Any]:
+ payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}")
+ if not isinstance(payload, dict):
+ raise RuntimeError("Failed to fetch GitHub PR metadata.")
+
+ return {
+ "number": int(payload["number"]),
+ "title": payload["title"],
+ "state": str(payload["state"]).upper(),
+ "head_branch": payload["head"]["ref"],
+ "base_branch": payload["base"]["ref"],
+ "url": payload["html_url"],
+ }
def resolve_pr_number(branch: str) -> int:
- query = urllib.parse.quote(f"is:pr head:{branch} sort:updated-desc")
- url = f"https://github.com/{OWNER}/{REPO}/pulls?q={query}"
- html_text = fetch_text(url)
- match = re.search(rf'/{OWNER}/{REPO}/pull/(\d+)', html_text)
- if match is None:
+ head_query = urllib.parse.quote(f"{OWNER}:{branch}")
+ payload, _ = fetch_json(f"https://api.github.com/repos/{OWNER}/{REPO}/pulls?state=all&head={head_query}")
+ if not isinstance(payload, list):
+ raise RuntimeError("Failed to resolve pull request from branch.")
+
+ matching_pull_requests = [item for item in payload if item.get("head", {}).get("ref") == branch]
+ if not matching_pull_requests:
raise RuntimeError(f"No public PR matched branch '{branch}'.")
- return int(match.group(1))
-
-def extract_embedded_data(html_text: str) -> dict[str, Any]:
- match = re.search(
- r'',
- html_text,
- re.S,
- )
- if match is None:
- raise RuntimeError("Failed to locate GitHub embedded PR metadata.")
- return json.loads(match.group(1))
-
-
-def extract_clipboard_values(html_text: str) -> list[str]:
- return [html.unescape(value) for value in re.findall(r']*\bvalue="(.*?)"', html_text, re.S)]
+ latest_pull_request = max(matching_pull_requests, key=lambda item: item.get("updated_at", ""))
+ return int(latest_pull_request["number"])
def collapse_whitespace(text: str) -> str:
@@ -208,49 +291,208 @@ def parse_test_report(block: str) -> dict[str, Any]:
return report
-def select_code_rabbit_summary(values: list[str]) -> str:
- for value in values:
- if "auto-generated comment: summarize by coderabbit.ai" in value:
- return value.strip()
- return ""
+def fetch_issue_comments(pr_number: int) -> list[dict[str, Any]]:
+ return fetch_paged_json(f"https://api.github.com/repos/{OWNER}/{REPO}/issues/{pr_number}/comments?per_page=100")
-def select_actionable_comments(values: list[str]) -> str:
- for value in values:
- if "Actionable comments posted:" in value and "Prompt for all review comments with AI agents" in value:
- return value.strip()
- return ""
+def select_latest_comment_body(
+ comments: list[dict[str, Any]],
+ predicate: Any,
+ required_user: str | None = None,
+) -> str:
+ matching_comments = []
+ for comment in comments:
+ body = html.unescape(str(comment.get("body", "")))
+ if required_user is not None and comment.get("user", {}).get("login") != required_user:
+ continue
+ if predicate(body):
+ comment_copy = dict(comment)
+ comment_copy["body"] = body
+ matching_comments.append(comment_copy)
+
+ if not matching_comments:
+ return ""
+
+ latest_comment = max(matching_comments, key=lambda item: (item.get("updated_at", ""), item.get("created_at", "")))
+ return str(latest_comment.get("body", "")).strip()
-def select_test_reports(values: list[str]) -> list[str]:
- return [value.strip() for value in values if "CTRF PR COMMENT TAG:" in value or "### Test Results" in value]
+def select_comment_bodies(
+ comments: list[dict[str, Any]],
+ predicate: Any,
+ required_user: str | None = None,
+) -> list[str]:
+ matching_comments = []
+ for comment in comments:
+ body = html.unescape(str(comment.get("body", "")))
+ if required_user is not None and comment.get("user", {}).get("login") != required_user:
+ continue
+ if predicate(body):
+ comment_copy = dict(comment)
+ comment_copy["body"] = body
+ matching_comments.append(comment_copy)
+
+ matching_comments.sort(key=lambda item: (item.get("created_at", ""), item.get("updated_at", "")))
+ return [str(comment.get("body", "")).strip() for comment in matching_comments]
-def build_result(pr_number: int, branch: str, html_text: str) -> dict[str, Any]:
- embedded_data = extract_embedded_data(html_text)
- pull_request = embedded_data["payload"]["pullRequestsLayoutRoute"]["pullRequest"]
- clipboard_values = extract_clipboard_values(html_text)
+def summarize_review_comment(comment: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "id": comment.get("id"),
+ "path": comment.get("path") or "",
+ "line": comment.get("line"),
+ "side": comment.get("side") or "",
+ "created_at": comment.get("created_at") or "",
+ "updated_at": comment.get("updated_at") or "",
+ "user": comment.get("user", {}).get("login") or "",
+ "commit_id": comment.get("commit_id") or "",
+ "in_reply_to_id": comment.get("in_reply_to_id"),
+ "body": comment.get("body") or "",
+ }
- summary_block = select_code_rabbit_summary(clipboard_values)
- actionable_block = select_actionable_comments(clipboard_values)
- test_blocks = select_test_reports(clipboard_values)
+def classify_review_thread_status(latest_comment: dict[str, Any]) -> str:
+ body = latest_comment.get("body") or ""
+ author = latest_comment.get("user") or ""
+ if author == CODERABBIT_LOGIN and REVIEW_COMMENT_ADDRESSED_MARKER in body:
+ return "addressed"
+ return "open"
+
+
+def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[dict[str, Any]]:
+ comment_threads: dict[int, dict[str, Any]] = {}
+
+ # GitHub review replies point to the root comment id. Grouping them first lets
+ # the skill surface the latest thread state instead of every historical reply.
+ for comment in sorted(comments, key=lambda item: (item.get("created_at") or "", item.get("id") or 0)):
+ comment_id = comment.get("id")
+ if comment_id is None:
+ continue
+
+ summary = summarize_review_comment(comment)
+ root_id = summary["in_reply_to_id"] or comment_id
+ thread = comment_threads.setdefault(
+ root_id,
+ {
+ "thread_id": root_id,
+ "path": summary["path"],
+ "line": summary["line"],
+ "root_comment": None,
+ "replies": [],
+ },
+ )
+
+ if summary["in_reply_to_id"] is None:
+ thread["root_comment"] = summary
+ thread["path"] = summary["path"]
+ thread["line"] = summary["line"]
+ else:
+ thread["replies"].append(summary)
+
+ threads: list[dict[str, Any]] = []
+ for thread in comment_threads.values():
+ root_comment = thread.get("root_comment")
+ if root_comment is None:
+ continue
+
+ ordered_comments = [root_comment, *thread["replies"]]
+ latest_comment = max(ordered_comments, key=lambda item: (item.get("updated_at") or "", item.get("created_at") or ""))
+ thread["latest_comment"] = latest_comment
+ thread["status"] = classify_review_thread_status(latest_comment)
+ threads.append(thread)
+
+ return sorted(threads, key=lambda item: (item["path"], item["line"] or 0, item["thread_id"]))
+
+
+def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
+ api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
+ commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
+ reviews = fetch_paged_json(f"{api_base}/reviews?per_page=100")
+ comments = fetch_paged_json(f"{api_base}/comments?per_page=100")
+
+ if not commits:
+ return {
+ "latest_commit": {},
+ "latest_review": {},
+ "threads": [],
+ "open_threads": [],
+ }
+
+ latest_commit = commits[-1]
+ latest_commit_sha = latest_commit.get("sha", "")
+ latest_commit_reviews = [
+ review for review in reviews if review.get("commit_id") == latest_commit_sha and review.get("submitted_at")
+ ]
+ candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
+ latest_review = (
+ max(candidate_reviews, key=lambda review: review.get("submitted_at", ""))
+ if candidate_reviews
+ else None
+ )
+
+ latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha]
+ threads = build_latest_commit_review_threads(latest_commit_comments)
+ open_threads = [thread for thread in threads if thread["status"] == "open"]
+
+ return {
+ "latest_commit": {
+ "sha": latest_commit_sha,
+ "message": latest_commit.get("commit", {}).get("message", ""),
+ },
+ "latest_review": {
+ "id": latest_review.get("id") if latest_review else None,
+ "state": latest_review.get("state") if latest_review else "",
+ "submitted_at": latest_review.get("submitted_at") if latest_review else "",
+ "commit_id": latest_review.get("commit_id") if latest_review else "",
+ "user": latest_review.get("user", {}).get("login") if latest_review else "",
+ "body": latest_review.get("body") if latest_review else "",
+ },
+ "threads": threads,
+ "open_threads": open_threads,
+ }
+
+
+def build_result(pr_number: int, branch: str) -> dict[str, Any]:
warnings: list[str] = []
+ pull_request_metadata = fetch_pull_request_metadata(pr_number)
+ issue_comments = fetch_issue_comments(pr_number)
+ summary_block = select_latest_comment_body(
+ issue_comments,
+ lambda body: "auto-generated comment: summarize by coderabbit.ai" in body,
+ required_user=CODERABBIT_LOGIN,
+ )
+ actionable_block = select_latest_comment_body(
+ issue_comments,
+ lambda body: "Actionable comments posted:" in body and "Prompt for all review comments with AI agents" in body,
+ required_user=CODERABBIT_LOGIN,
+ )
+ test_blocks = select_comment_bodies(
+ issue_comments,
+ lambda body: "CTRF PR COMMENT TAG:" in body or "### Test Results" in body,
+ )
+
if not summary_block:
- warnings.append("CodeRabbit summary block was not found.")
- if not actionable_block:
- warnings.append("CodeRabbit actionable comments block was not found.")
+ warnings.append("CodeRabbit summary block was not found in issue comments.")
if not test_blocks:
- warnings.append("PR test-report block was not found.")
+ warnings.append("PR test-report block was not found in issue comments.")
+
+ latest_commit_review: dict[str, Any] = {}
+ try:
+ latest_commit_review = fetch_latest_commit_review(pr_number)
+ except Exception as error: # noqa: BLE001
+ warnings.append(f"Latest commit review comments could not be fetched: {error}")
+
+ if not actionable_block and not latest_commit_review.get("threads"):
+ warnings.append("CodeRabbit actionable comments block was not found in issue comments.")
return {
"pull_request": {
- "number": int(pull_request["number"]),
- "title": pull_request["title"],
- "state": pull_request["state"],
- "head_branch": pull_request["headBranch"],
- "base_branch": pull_request["baseBranch"],
- "url": f"https://github.com/{OWNER}/{REPO}/pull/{pr_number}",
+ "number": pull_request_metadata["number"],
+ "title": pull_request_metadata["title"],
+ "state": pull_request_metadata["state"],
+ "head_branch": pull_request_metadata["head_branch"],
+ "base_branch": pull_request_metadata["base_branch"],
+ "url": pull_request_metadata["url"],
"resolved_from_branch": branch,
},
"coderabbit_summary": {
@@ -258,6 +500,7 @@ def build_result(pr_number: int, branch: str, html_text: str) -> dict[str, Any]:
"raw": summary_block,
},
"coderabbit_comments": parse_actionable_comments(actionable_block) if actionable_block else {},
+ "latest_commit_review": latest_commit_review,
"test_reports": [parse_test_report(block) for block in test_blocks],
"parse_warnings": warnings,
}
@@ -289,6 +532,32 @@ def format_text(result: dict[str, Any]) -> str:
if comment["description"]:
lines.append(f" Description: {comment['description']}")
+ 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:
+ lines.append("")
+ lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
+ if latest_review:
+ lines.append(
+ "Latest review: "
+ f"{latest_review.get('state', '')} by {latest_review.get('user', '')} "
+ f"at {latest_review.get('submitted_at', '')}"
+ )
+
+ lines.append(
+ "Latest commit review threads: "
+ f"{len(latest_commit_review.get('threads', []))} total, {len(open_threads)} open"
+ )
+ for thread in 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'])}")
+ if latest_comment["id"] != root_comment["id"]:
+ lines.append(f" Latest by {latest_comment['user']}: {collapse_whitespace(latest_comment['body'])}")
+
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
@@ -327,11 +596,14 @@ def parse_args() -> argparse.Namespace:
def main() -> None:
args = parse_args()
- branch = args.branch or get_current_branch()
- pr_number = args.pr or resolve_pr_number(branch)
- url = f"https://github.com/{OWNER}/{REPO}/pull/{pr_number}"
- html_text = fetch_text(url)
- result = build_result(pr_number, branch, html_text)
+ if args.pr is not None:
+ pr_number = args.pr
+ branch = args.branch or ""
+ else:
+ branch = args.branch or get_current_branch()
+ pr_number = resolve_pr_number(branch)
+
+ result = build_result(pr_number, branch)
if args.format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
diff --git a/ai-plan/public/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/todos/cqrs-rewrite-migration-tracking.md
index 1056d48c..a958acfe 100644
--- a/ai-plan/public/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/todos/cqrs-rewrite-migration-tracking.md
@@ -399,10 +399,10 @@
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` 在 CQRS generator MVP 后通过
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~GFramework.SourceGenerators.Tests.Cqrs.CqrsHandlerRegistryGeneratorTests"` 通过,`2` 个测试全部通过
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~GFramework.Core.Tests.Cqrs.CqrsHandlerRegistrarTests|FullyQualifiedName~GFramework.Core.Tests.Architectures.ArchitectureModulesBehaviorTests|FullyQualifiedName~GFramework.Core.Tests.Mediator.MediatorArchitectureIntegrationTests|FullyQualifiedName~GFramework.Core.Tests.Mediator.MediatorComprehensiveTests"` 通过,`41` 个测试全部通过
- - `dotnet build GFramework/GFramework.sln -c Release`
- 在当前 WSL 环境下命中既有 `GFramework.csproj` NuGet fallback package folder 配置问题
- (`D:\\Tool\\Development Tools\\Microsoft Visual Studio\\Shared\\NuGetPackages`),
- 与本轮 CQRS 改动无关;`GFramework.Core.Tests` 相关项目构建与回归已通过
+- `dotnet build GFramework/GFramework.sln -c Release`
+ 在当前 WSL 环境下命中既有 `GFramework.csproj` NuGet fallback package folder 配置问题
+ (机器本地路径已省略),
+ 与本轮 CQRS 改动无关;`GFramework.Core.Tests` 相关项目构建与回归已通过
- `dotnet build GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
在显式额外程序集 CQRS 注册入口落地后通过,仅存在既有 `MA0048` warnings
- `dotnet test GFramework/GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~GFramework.Core.Tests.Architectures.ArchitectureAdditionalCqrsHandlersTests|FullyQualifiedName~GFramework.Core.Tests.Architectures.ArchitectureModulesBehaviorTests|FullyQualifiedName~GFramework.Core.Tests.Cqrs.CqrsHandlerRegistrarTests|FullyQualifiedName~GFramework.Core.Tests.Architectures.RegistryInitializationHookBaseTests"`
@@ -469,7 +469,7 @@
若本轮中断,优先从以下顺序恢复:
1. 查看 `ai-plan/public/traces/cqrs-rewrite-migration-trace.md`
-2. 确认当前恢复点 `CQRS-REWRITE-RP-015` 已对应到最新提交
+2. 确认当前恢复点 `CQRS-REWRITE-RP-042` 已对应到最新提交
3. 优先继续执行 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 中的 Phase 7:
- 先决定是否正式支持旧 `GFramework.Core.Abstractions.Cqrs*` / `GFramework.Core.Cqrs.Extensions` public namespace 兼容,还是明确要求消费端迁到当前 `GFramework.Cqrs*` 路径
- 再评估 `CqrsCoroutineExtensions` 是否保留在 `GFramework.Core`,或连同所需协程辅助一起形成更小的可迁移边界
@@ -701,9 +701,9 @@
- 已新增项目级 `$gframework-pr-review` skill:
- 目录:`.codex/skills/gframework-pr-review/`
- - 作用:定位当前分支对应的 GitHub PR,并直接从 PR 页面提取 CodeRabbit 评论、`Failed checks`
- 与 CTRF 测试结果
- - 约束:不依赖 `gh` CLI;默认通过公开 GitHub HTML 页面工作
+ - 作用:定位当前分支对应的 GitHub PR,并优先通过 GitHub PR / issue comments / review comments API 提取
+ CodeRabbit 汇总、最新 head commit review threads、`Failed checks` 与 CTRF 测试结果
+ - 约束:不依赖 `gh` CLI;不再把重型 PR HTML 页面当作主数据源
- 已根据 PR `#253` 的公开 review 内容完成本地修正:
- `.codex/skills/gframework-boot/SKILL.md` 的恢复 heuristics 不再把 `next step/continue/继续`
直接映射为 `recovery`
@@ -711,6 +711,14 @@
tracking document”
- `Godot/script_templates/Node/*.cs` 与 `GFramework.Core.Abstractions/Controller/IController.cs`
中旧 `Rule` 命名空间残留已同步修正
+ - `fetch_current_pr_review.py` 已改为:
+ - Git 路径支持环境变量覆盖并回退到 `git.exe` / `git`
+ - `--pr` 模式不再强制读取当前分支
+ - `--branch` 到 PR 编号的解析改为走 GitHub PR API
+ - CodeRabbit summary / CTRF 测试报告改为走 issue comments API
+ - 最新 review 依据改为 latest head commit review threads,而不是只看汇总块
+ - `ai-plan/public/todos/cqrs-rewrite-migration-tracking.md` 已移除公开文档中的机器本地绝对路径,并统一
+ 下次恢复建议里的恢复点编号
### 阶段:RP-042 验证
@@ -719,13 +727,16 @@
- 备注:`fetch_current_pr_review.py` 语法正确
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
- 结果:通过
- - 备注:解析出 2 条 CodeRabbit 待处理评论、1 条 `Title check` warning,以及 `2103 passed / 0 failed`
- 的测试结果
+ - 备注:已通过 API-first 路径解析 PR 元数据、latest head commit review threads、CodeRabbit summary
+ 与 CTRF 测试结果,不再依赖 PR HTML
+- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch feat/cqrs-optimization`
+ - 结果:通过
+ - 备注:已验证 branch -> PR 解析同样通过 GitHub API 工作
- `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release`
- 结果:通过
- 备注:相关 C# 改动已完成构建验证
### 下一步
-1. 若需要继续处理 PR `#253` 的最后一项 `Title check` warning,应在 GitHub 页面上直接修改 PR 标题
-2. 若后续仍采用 PR 驱动修复流程,优先使用 `$gframework-pr-review` 复查当前分支 PR 的 CodeRabbit 评论和测试状态
+1. 若要让 PR `#253` 上的 latest head review threads 反映本轮本地修正,需要先提交并推送当前分支,再重新执行 `$gframework-pr-review`
+2. PR 当前公开 warning 仍包含 `Docstring Coverage`,若后续要继续消除此项,需要单独规划并提交文档注释覆盖率改进
diff --git a/ai-plan/public/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/traces/cqrs-rewrite-migration-trace.md
index e9039439..3696ff6c 100644
--- a/ai-plan/public/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/traces/cqrs-rewrite-migration-trace.md
@@ -1371,14 +1371,17 @@
- 建立 `CQRS-REWRITE-RP-042` 恢复点
- 新增项目级 skill `.codex/skills/gframework-pr-review/`:
- 暗号为 `$gframework-pr-review`
- - 使用 Windows Git 解析当前分支,并通过公开 GitHub PR 页面定位当前分支对应的 PR
- - 直接从 PR HTML 中提取 `Summary by CodeRabbit`、`Actionable comments posted`、`Failed checks` 与 CTRF 测试结果
+ - 使用 Windows Git 解析当前分支,并通过 GitHub PR API 定位当前分支对应的 PR
+ - 通过 GitHub issue comments / reviews / review comments API 提取 `Summary by CodeRabbit`、最新 head
+ commit review threads、`Failed checks` 与 CTRF 测试结果
+ - 不再把重型 PR HTML 页面作为主数据源,只在调试或兼容场景下保留为兜底思路
- 不依赖 `gh` CLI,也不要求登录态;脚本会显式绕过当前 shell 中失效的代理变量
- 用新脚本验证了 PR `#253` 的当前状态:
- - `CodeRabbit actionable comments` 仍有 2 条真实待处理项,分别落在 `.codex/skills/gframework-boot/SKILL.md`
- 与 `AGENTS.md`
+ - latest head commit review threads 已可直接从 API 提取;在远端最新提交未更新前,当前仍显示 4 条 open
+ threads,其中 2 条落在 `fetch_current_pr_review.py`、2 条落在 `ai-plan/public/todos/cqrs-rewrite-migration-tracking.md`
- PR 页面当前无 `Failed Tests`,CTRF 测试报告显示 `2103 passed / 0 failed`
- - `Failed checks` 仅剩 `Title check` warning,属于 GitHub PR 标题元数据问题,不是本地代码缺陷
+ - `Failed checks` 当前可稳定提取到 `Docstring Coverage` warning;该项属于 PR 级文档注释覆盖率问题,不是 FPR
+ skill 解析链路故障
- 已按 PR `#253` 的公开建议完成本地修正:
- `gframework-boot` 的恢复 heuristics 改为“先检索 `ai-plan/`,再判定 `resume` 或 `recovery`”
- `AGENTS.md` 将 `ai-libs/**` 观察写入 active plan/trace 的要求收窄到“多步/复杂任务或已有 active tracking document”
@@ -1393,7 +1396,11 @@
- 备注:`fetch_current_pr_review.py` 语法正确,且避免了只读文件系统下写 `__pycache__` 的问题
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
- 结果:通过
- - 备注:成功解析当前 PR 元数据、2 条 CodeRabbit 待处理评论、1 条 `Title check` warning 和 1 组 CTRF 测试报告
+ - 备注:成功通过 API-first 路径解析当前 PR 元数据、latest head commit review threads、`Docstring Coverage`
+ warning 和 CTRF 测试报告
+- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch feat/cqrs-optimization`
+ - 结果:通过
+ - 备注:验证 branch -> PR 解析也已摆脱 HTML 搜索
- `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release`
- 结果:通过
- 备注:`GFramework.Cqrs.Abstractions` 与 `GFramework.Core.Abstractions` 均成功构建,0 warning / 0 error
@@ -1404,4 +1411,4 @@
### 下一步
1. 若继续沿用当前 PR 驱动修复流程,可直接用 `$gframework-pr-review` 复查后续 PR 的 CodeRabbit 评论与测试状态
-2. 若要消除 PR `#253` 的最后一个 `Title check` warning,需要在 GitHub 上手动修改 PR 标题;该项不属于仓库内代码修复范围
+2. 若要验证本轮本地修正已经消除远端 latest head review threads,需要在提交并推送当前分支后重新执行 `$gframework-pr-review`