From 5e346189eed4058c73c833ca72de3f91f901c9c4 Mon Sep 17 00:00:00 2001
From: DandreYang <13072547+Dandre126@users.noreply.github.com>
Date: Sun, 13 Sep 2026 20:34:38 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat(review):=20=E6=B7=BB=E5=8A=A0=E5=BC=80?=
=?UTF-8?q?=E5=8F=91=E7=BA=BF=E5=A2=9E=E9=87=8F=E5=AE=A1=E6=9F=A5=E9=9D=B6?=
=?UTF-8?q?=E5=8C=BA=E6=8F=90=E5=8F=96=E4=B8=8E=E6=9C=AC=E5=9C=B0=E9=97=A8?=
=?UTF-8?q?=E7=A6=81=E6=A0=B8=E9=AA=8C=20(dyro=20review=20verify/pack)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/dyro/cli.py | 57 +++++++
src/dyro/review_pack.py | 343 ++++++++++++++++++++++++++++++++++++++
tests/test_review_pack.py | 102 ++++++++++++
3 files changed, 502 insertions(+)
create mode 100644 src/dyro/review_pack.py
create mode 100644 tests/test_review_pack.py
diff --git a/src/dyro/cli.py b/src/dyro/cli.py
index 5980f9c..16737b5 100644
--- a/src/dyro/cli.py
+++ b/src/dyro/cli.py
@@ -3232,6 +3232,49 @@ def cmd_changeset_verify(args: argparse.Namespace) -> None:
raise DyroError(f"Change Set {args.id} 未通过核验")
+def cmd_review_verify(args: argparse.Namespace) -> None:
+ from .review_pack import run_line_verify
+ from .workspace import get_line
+
+ config = _config(args)
+ line = get_line(config, args.line)
+ results = run_line_verify(config, line, dry_run=args.dry_run)
+ if not results:
+ print(f"开发线 {line.id} 无已配置门禁指令。")
+ return
+ failures = [r for r in results if not r.passed]
+ for r in results:
+ status_str = "PASS" if r.passed else "FAIL"
+ print(f"[{status_str}] {r.repo_id}: {' '.join(r.argv)} ({r.elapsed_seconds:.2f}s)")
+ if not r.passed and r.stdout:
+ print(f" --- 失败日志摘录 ---\n " + "\n ".join(r.stdout.splitlines()[-15:]))
+ if failures:
+ raise DyroError(f"开发线 {line.id} 本地静态门禁未通过({len(failures)}/{len(results)} 失败)")
+ print(f"\n全部门禁通过(共 {len(results)} 项)。")
+
+
+def cmd_review_pack(args: argparse.Namespace) -> None:
+ from .review_pack import build_review_pack
+ from pathlib import Path
+
+ config = _config(args)
+ content = build_review_pack(
+ config,
+ args.line,
+ base=args.base,
+ run_verify=not args.no_verify,
+ max_lines_per_file=args.max_lines,
+ dry_run=args.dry_run,
+ )
+ if args.out:
+ out_path = Path(args.out)
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ out_path.write_text(content, encoding="utf-8")
+ print(f"已生成审查靶区包:{out_path}")
+ else:
+ print(content)
+
+
def cmd_task_create(args: argparse.Namespace) -> None:
config = _config(args)
validate_id(args.id, "任务 ID")
@@ -5353,6 +5396,20 @@ def build_parser() -> argparse.ArgumentParser:
)
changeset_verify.set_defaults(func=cmd_changeset_verify)
+ review = sub.add_parser("review", help="开发线增量审查靶区提取与门禁核验")
+ review_sub = review.add_subparsers(dest="review_command", required=True)
+ review_verify = review_sub.add_parser("verify", help="运行开发线各仓本地 verify 静态门禁")
+ review_verify.add_argument("--line", required=True, help="开发线 ID")
+ review_verify.set_defaults(func=cmd_review_verify)
+
+ review_pack = review_sub.add_parser("pack", help="提取开发线跨仓精简 Diff 与审查上下文")
+ review_pack.add_argument("--line", required=True, help="开发线 ID")
+ review_pack.add_argument("--base", help="对比基线;默认开发线 base")
+ review_pack.add_argument("--no-verify", action="store_true", help="跳过本地 verify 门禁检查")
+ review_pack.add_argument("--max-lines", type=int, default=250, help="单文件 Diff 最大保留行数(防 Token 膨胀)")
+ review_pack.add_argument("--out", help="输出 Markdown 审查包文件路径;默认标准输出")
+ review_pack.set_defaults(func=cmd_review_pack)
+
objective = sub.add_parser(
"objective", help="持久化并观察一个跨任务 Objective;此阶段不执行 Task"
)
diff --git a/src/dyro/review_pack.py b/src/dyro/review_pack.py
new file mode 100644
index 0000000..56aeab5
--- /dev/null
+++ b/src/dyro/review_pack.py
@@ -0,0 +1,343 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime, timezone
+import fnmatch
+from pathlib import Path
+import time
+from typing import Sequence
+
+from .config import Config
+from .errors import DyroError, ValidationError
+from .process import git, run
+from .workspace import get_line, line_repository_path, Line
+
+IGNORE_DIFF_PATTERNS = (
+ "package-lock.json",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ "uv.lock",
+ "poetry.lock",
+ "*.min.js",
+ "*.min.css",
+ "*.map",
+ "*.svg",
+ "*.png",
+ "*.jpg",
+ "*.jpeg",
+ "*.ico",
+ "*.pdf",
+ "*.log",
+ "dist/*",
+ "build/*",
+ "target/*",
+ "unpackage/*",
+ ".pytest_cache/*",
+ ".idea/*",
+ ".vscode/*",
+)
+
+CONTRACT_PATTERNS = (
+ "*dto*",
+ "*schema*",
+ "*schemas*",
+ "*model*",
+ "*models*",
+ "*entity*",
+ "*entities*",
+ "*api*",
+ "*controller*",
+ "*protocol*",
+ "*contract*",
+ "*constants*",
+ "*.sql",
+ "*ddl*",
+)
+
+
+@dataclass(frozen=True)
+class GateResult:
+ repo_id: str
+ argv: tuple[str, ...]
+ code: int
+ passed: bool
+ stdout: str
+ elapsed_seconds: float
+
+
+@dataclass(frozen=True)
+class RepoDiffSummary:
+ repo_id: str
+ branch: str
+ base: str
+ commits_count: int
+ commits_log: str
+ stat_summary: str
+ contract_files: tuple[str, ...]
+ other_files: tuple[str, ...]
+ file_diffs: dict[str, str]
+
+
+def is_ignored(filename: str) -> bool:
+ norm = filename.replace("\\", "/")
+ for pattern in IGNORE_DIFF_PATTERNS:
+ if fnmatch.fnmatch(norm, pattern) or fnmatch.fnmatch(Path(norm).name, pattern):
+ return True
+ return False
+
+
+def is_contract(filename: str) -> bool:
+ norm = filename.replace("\\", "/").lower()
+ for pattern in CONTRACT_PATTERNS:
+ if fnmatch.fnmatch(norm, pattern) or fnmatch.fnmatch(Path(norm).name, pattern):
+ return True
+ return False
+
+
+def run_line_verify(
+ config: Config,
+ line: Line,
+ *,
+ dry_run: bool = False,
+) -> list[GateResult]:
+ results: list[GateResult] = []
+ for repo_id in line.repositories:
+ repo_cfg = config.repositories.get(repo_id)
+ if not repo_cfg or not repo_cfg.verify:
+ continue
+ repo_path = line_repository_path(config, line, repo_id)
+ if not repo_path.is_dir():
+ continue
+ for cmd in repo_cfg.verify:
+ start_time = time.perf_counter()
+ exec_res = run(cmd, cwd=repo_path, dry_run=dry_run)
+ elapsed = time.perf_counter() - start_time
+ passed = exec_res.code == 0
+ results.append(
+ GateResult(
+ repo_id=repo_id,
+ argv=tuple(cmd),
+ code=exec_res.code,
+ passed=passed,
+ stdout=exec_res.stdout.strip(),
+ elapsed_seconds=elapsed,
+ )
+ )
+ return results
+
+
+def analyze_repo_diff(
+ config: Config,
+ line: Line,
+ repo_id: str,
+ *,
+ base: str | None = None,
+ max_lines_per_file: int = 250,
+) -> RepoDiffSummary | None:
+ repo_path = line_repository_path(config, line, repo_id)
+ if not repo_path.is_dir():
+ return None
+
+ actual_base = base or line.base_for(repo_id)
+
+ # Check if base ref exists
+ verify_base = git(repo_path, "rev-parse", "--verify", "-q", actual_base)
+ if verify_base.code != 0:
+ # Fallback to origin/base if local doesn't exist
+ alt_base = f"origin/{actual_base}"
+ if git(repo_path, "rev-parse", "--verify", "-q", alt_base).code == 0:
+ actual_base = alt_base
+
+ branch_res = git(repo_path, "branch", "--show-current")
+ branch = branch_res.stdout.strip() or line.branch
+
+ log_res = git(repo_path, "log", "--oneline", f"{actual_base}...HEAD")
+ commits = [line for line in log_res.stdout.strip().splitlines() if line.strip()]
+ commits_count = len(commits)
+ commits_log = "\n".join(commits[:15])
+ if commits_count > 15:
+ commits_log += f"\n... (共 {commits_count} 个提交,省略剩余 {commits_count - 15} 个)"
+
+ stat_res = git(repo_path, "diff", f"{actual_base}...HEAD", "--stat")
+ stat_summary = stat_res.stdout.strip()
+
+ names_res = git(repo_path, "diff", f"{actual_base}...HEAD", "--name-status")
+ changed_names = [line.strip() for line in names_res.stdout.strip().splitlines() if line.strip()]
+
+ contract_files: list[str] = []
+ other_files: list[str] = []
+ file_diffs: dict[str, str] = {}
+
+ for line_item in changed_names:
+ parts = line_item.split(maxsplit=1)
+ if len(parts) < 2:
+ continue
+ status, file_path = parts[0], parts[1]
+ # For rename, parts[1] might contain "old -> new"
+ if " -> " in file_path:
+ file_path = file_path.split(" -> ")[1].strip()
+
+ if is_ignored(file_path):
+ continue
+
+ if is_contract(file_path):
+ contract_files.append(file_path)
+ else:
+ other_files.append(file_path)
+
+ # Get diff for this single file
+ diff_res = git(repo_path, "diff", f"{actual_base}...HEAD", "--", file_path)
+ raw_diff = diff_res.stdout
+ diff_lines = raw_diff.splitlines()
+
+ if len(diff_lines) > max_lines_per_file:
+ truncated_diff = "\n".join(diff_lines[:max_lines_per_file])
+ truncated_diff += f"\n... [Dyro Review Pack 提示:该文件改动超过 {max_lines_per_file} 行,已截断以防止 Token 膨胀。共 {len(diff_lines)} 行]"
+ file_diffs[file_path] = truncated_diff
+ else:
+ file_diffs[file_path] = raw_diff
+
+ return RepoDiffSummary(
+ repo_id=repo_id,
+ branch=branch,
+ base=actual_base,
+ commits_count=commits_count,
+ commits_log=commits_log,
+ stat_summary=stat_summary,
+ contract_files=tuple(contract_files),
+ other_files=tuple(other_files),
+ file_diffs=file_diffs,
+ )
+
+
+def build_review_pack(
+ config: Config,
+ line_id: str,
+ *,
+ base: str | None = None,
+ run_verify: bool = True,
+ max_lines_per_file: int = 250,
+ dry_run: bool = False,
+) -> str:
+ line = get_line(config, line_id)
+ now_str = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
+
+ sections: list[str] = [
+ f"# Dyro 开发线复核审查靶区包(Review Pack)",
+ f"- **开发线 ID**:`{line.id}`",
+ f"- **分支**:`{line.branch}`",
+ f"- **对比基线(Base)**:`{base or line.base}`",
+ f"- **生成时间**:{now_str}",
+ "",
+ ]
+
+ # 1. Verification Gates
+ gates_failed = False
+ if run_verify:
+ gate_results = run_line_verify(config, line, dry_run=dry_run)
+ sections.append("## 一、 本地静态门禁验证(Verify Gates)")
+ if not gate_results:
+ sections.append("> ℹ️ 当前开发线仓库未配置或未执行本地 verify 门禁。\n")
+ else:
+ sections.append("| 仓库 | 门禁指令 | 耗时 | 状态 |")
+ sections.append("|:---|:---|:---|:---|")
+ for gr in gate_results:
+ status_label = "✅ PASS" if gr.passed else "❌ FAIL"
+ cmd_str = " ".join(gr.argv)
+ sections.append(f"| `{gr.repo_id}` | `{cmd_str}` | {gr.elapsed_seconds:.2f}s | {status_label} |")
+ if not gr.passed:
+ gates_failed = True
+
+ sections.append("")
+ if gates_failed:
+ sections.append("> 🚨 **门禁阻断告警**:本地静态门禁未全部通过!请优先解决机器检查报错,避免消耗 AI Token 进行无意义的肉眼语法排查。\n")
+ sections.append("### 门禁失败日志摘录")
+ for gr in gate_results:
+ if not gr.passed:
+ snippet = "\n".join(gr.stdout.splitlines()[-25:])
+ sections.append(f"#### `{gr.repo_id}`: `{' '.join(gr.argv)}`")
+ sections.append(f"```text\n{snippet}\n```\n")
+
+ # 2. Repo Diff Analysis
+ repo_summaries: list[RepoDiffSummary] = []
+ for repo_id in line.repositories:
+ summary = analyze_repo_diff(
+ config,
+ line,
+ repo_id,
+ base=base,
+ max_lines_per_file=max_lines_per_file,
+ )
+ if summary is not None:
+ repo_summaries.append(summary)
+
+ sections.append("## 二、 跨仓改动总览(Cross-Repo Diff Overview)")
+ sections.append("| 仓库 | 分支 | 基线 | 提交数 | 契约文件数 | 业务文件数 |")
+ sections.append("|:---|:---|:---|:---|:---|:---|")
+ total_commits = 0
+ total_contracts = 0
+ total_others = 0
+
+ for rs in repo_summaries:
+ c_count = len(rs.contract_files)
+ o_count = len(rs.other_files)
+ total_commits += rs.commits_count
+ total_contracts += c_count
+ total_others += o_count
+ sections.append(f"| `{rs.repo_id}` | `{rs.branch}` | `{rs.base}` | {rs.commits_count} | {c_count} | {o_count} |")
+
+ sections.append("")
+ sections.append(f"**合计**:共 {len(repo_summaries)} 个仓库,{total_commits} 次提交,{total_contracts} 个核心契约变更,{total_others} 个业务实现变更。\n")
+
+ # 3. High-Priority Contract Index
+ sections.append("## 三、 核心协议与契约变更索引(高危审查靶区)")
+ has_any_contract = False
+ for rs in repo_summaries:
+ if rs.contract_files:
+ has_any_contract = True
+ sections.append(f"### 仓 `{rs.repo_id}` 契约文件({len(rs.contract_files)} 个):")
+ for cf in rs.contract_files:
+ sections.append(f"- `{cf}`")
+ sections.append("")
+
+ if not has_any_contract:
+ sections.append("> 无显式 DTO / Schema / API 契约文件名变更。\n")
+
+ # 4. Recommended Review Seats
+ sections.append("## 四、 分席位独立审查建议(阻断上下文二次方膨胀)")
+ sections.append("> 💡 **审查指南**:不要把整个大包塞进同一个 AI 会话。推荐使用独立对话分别委派以下三个席位,各席位完成审查后向主会话仅汇总结论:\n")
+ sections.append(
+ "1. **`Reviewer-Java` 席位**:专注于 `common-msv` 仓的 DDL、业务事务回滚、鉴权注解与对外接口边界。\n"
+ "2. **`Reviewer-Python` 席位**:专注于 `ai-agent` 仓的模型路由、Prompt 构造、Pydantic Schema 校验与供应商容灾。\n"
+ "3. **`Reviewer-Contract` 席位**:专注于 Java DTO ↔ Python Schema ↔ 前端(小程序/PC)实时态与历史态消费者对齐。\n"
+ )
+
+ # 5. Compact Code Diffs
+ sections.append("## 五、 精简增量代码 Diff(Compact Diff)")
+ for rs in repo_summaries:
+ if not rs.file_diffs:
+ continue
+ sections.append(f"### 仓库 `{rs.repo_id}`")
+ if rs.commits_log:
+ sections.append("最近提交记录(点击展开)
\n")
+ sections.append(f"```text\n{rs.commits_log}\n```\n \n")
+
+ # Contract diffs first
+ if rs.contract_files:
+ sections.append(f"#### 1. 契约与协议代码 Diff({len(rs.contract_files)} 个)")
+ for cf in rs.contract_files:
+ diff_text = rs.file_diffs.get(cf, "")
+ if diff_text.strip():
+ sections.append(f"**文件:`{cf}`**")
+ sections.append(f"```diff\n{diff_text.strip()}\n```\n")
+
+ # Other diffs
+ if rs.other_files:
+ sections.append(f"#### 2. 业务实现代码 Diff({len(rs.other_files)} 个)")
+ for of in rs.other_files:
+ diff_text = rs.file_diffs.get(of, "")
+ if diff_text.strip():
+ sections.append(f"{of}(点击展开 Diff)
\n")
+ sections.append(f"```diff\n{diff_text.strip()}\n```\n \n")
+
+ return "\n".join(sections)
diff --git a/tests/test_review_pack.py b/tests/test_review_pack.py
new file mode 100644
index 0000000..548b81c
--- /dev/null
+++ b/tests/test_review_pack.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+from contextlib import redirect_stdout
+from io import StringIO
+from pathlib import Path
+
+from dyro.config import load
+from dyro.review_pack import build_review_pack, run_line_verify
+from dyro.workspace import create_line, line_repository_path
+
+from tests.support import WorkspaceCase, shell
+
+
+def commit_file(worktree: Path, name: str, message: str, content: str = "") -> None:
+ target = worktree / name
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_text(content or f"{message}\n", encoding="utf-8")
+ shell("git", "add", str(target), cwd=worktree)
+ shell("git", "commit", "-m", message, cwd=worktree)
+
+
+class ReviewPackTests(WorkspaceCase):
+ def test_run_line_verify_pass_and_fail(self) -> None:
+ config_path = self.root / "dyro.toml"
+ config_path.write_text(
+ config_path.read_text(encoding="utf-8").replace(
+ 'verify = [["git", "diff", "--check"]]',
+ 'verify = [["git", "status"], ["sh", "-c", "exit 0"]]',
+ ),
+ encoding="utf-8",
+ )
+ config = load(self.root)
+ create_line(config, line_id="feat-1", branch="feat/1", base="main")
+ from dyro.workspace import get_line
+
+ line = get_line(config, "feat-1")
+ results = run_line_verify(config, line)
+ self.assertEqual(len(results), 2)
+ self.assertTrue(all(r.passed for r in results))
+
+ def test_build_review_pack_generates_markdown_with_contracts(self) -> None:
+ config_path = self.root / "dyro.toml"
+ config_path.write_text(
+ config_path.read_text(encoding="utf-8").replace(
+ 'verify = [["git", "diff", "--check"]]',
+ 'verify = [["sh", "-c", "echo all-good"]]',
+ ),
+ encoding="utf-8",
+ )
+ config = load(self.root)
+ create_line(config, line_id="feat-review", branch="feat/review", base="main")
+
+ from dyro.workspace import get_line
+
+ line = get_line(config, "feat-review")
+ api_path = line_repository_path(config, line, "api")
+
+ # Commit a contract change and an implementation change
+ commit_file(
+ api_path,
+ "src/dto/UserDTO.java",
+ "feat(api): add UserDTO",
+ "public class UserDTO { String id; }",
+ )
+ commit_file(
+ api_path,
+ "src/service/UserService.java",
+ "feat(api): update service",
+ "class UserService {}",
+ )
+
+ pack = build_review_pack(config, "feat-review", run_verify=True)
+ self.assertIn("Dyro 开发线复核审查靶区包", pack)
+ self.assertIn("UserDTO.java", pack)
+ self.assertIn("UserService.java", pack)
+ self.assertIn("✅ PASS", pack)
+ self.assertIn("Reviewer-Java", pack)
+ self.assertIn("Reviewer-Contract", pack)
+
+ def test_cli_review_pack_and_verify(self) -> None:
+ from dyro.cli import main
+
+ config_path = self.root / "dyro.toml"
+ config_path.write_text(
+ config_path.read_text(encoding="utf-8").replace(
+ 'verify = [["git", "diff", "--check"]]',
+ 'verify = [["sh", "-c", "echo verify-ok"]]',
+ ),
+ encoding="utf-8",
+ )
+ config = load(self.root)
+ create_line(config, line_id="feat-cli", branch="feat/cli", base="main")
+
+ buf = StringIO()
+ with redirect_stdout(buf):
+ main(["--root", str(self.root), "review", "verify", "--line", "feat-cli"])
+ self.assertIn("全部门禁通过", buf.getvalue())
+
+ buf_pack = StringIO()
+ with redirect_stdout(buf_pack):
+ main(["--root", str(self.root), "review", "pack", "--line", "feat-cli"])
+ self.assertIn("Dyro 开发线复核审查靶区包", buf_pack.getvalue())
From d17df5f2311080b53b9879acfba5e3e83086efc1 Mon Sep 17 00:00:00 2001
From: DandreYang <13072547+Dandre126@users.noreply.github.com>
Date: Sun, 13 Sep 2026 20:39:49 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat(review):=20=E5=A2=9E=E5=8A=A0=20--scop?=
=?UTF-8?q?e=20=E6=94=AF=E6=8C=81=20(contracts/all/summary)=20=E6=99=BA?=
=?UTF-8?q?=E8=83=BD=E8=A3=81=E5=89=AA=20Diff?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/dyro/cli.py | 7 ++++
src/dyro/review_pack.py | 83 ++++++++++++++++++++++++++---------------
2 files changed, 59 insertions(+), 31 deletions(-)
diff --git a/src/dyro/cli.py b/src/dyro/cli.py
index 16737b5..d8a0c0f 100644
--- a/src/dyro/cli.py
+++ b/src/dyro/cli.py
@@ -3262,6 +3262,7 @@ def cmd_review_pack(args: argparse.Namespace) -> None:
config,
args.line,
base=args.base,
+ scope=args.scope,
run_verify=not args.no_verify,
max_lines_per_file=args.max_lines,
dry_run=args.dry_run,
@@ -5405,6 +5406,12 @@ def build_parser() -> argparse.ArgumentParser:
review_pack = review_sub.add_parser("pack", help="提取开发线跨仓精简 Diff 与审查上下文")
review_pack.add_argument("--line", required=True, help="开发线 ID")
review_pack.add_argument("--base", help="对比基线;默认开发线 base")
+ review_pack.add_argument(
+ "--scope",
+ choices=("contracts", "all", "summary"),
+ default="contracts",
+ help="审查靶区范围:contracts (仅核心契约/协议Diff,默认), all (全量文件Diff), summary (仅改动索引不含Diff)",
+ )
review_pack.add_argument("--no-verify", action="store_true", help="跳过本地 verify 门禁检查")
review_pack.add_argument("--max-lines", type=int, default=250, help="单文件 Diff 最大保留行数(防 Token 膨胀)")
review_pack.add_argument("--out", help="输出 Markdown 审查包文件路径;默认标准输出")
diff --git a/src/dyro/review_pack.py b/src/dyro/review_pack.py
index 56aeab5..d23d2d5 100644
--- a/src/dyro/review_pack.py
+++ b/src/dyro/review_pack.py
@@ -132,6 +132,7 @@ def analyze_repo_diff(
repo_id: str,
*,
base: str | None = None,
+ scope: str = "contracts",
max_lines_per_file: int = 250,
) -> RepoDiffSummary | None:
repo_path = line_repository_path(config, line, repo_id)
@@ -140,10 +141,8 @@ def analyze_repo_diff(
actual_base = base or line.base_for(repo_id)
- # Check if base ref exists
verify_base = git(repo_path, "rev-parse", "--verify", "-q", actual_base)
if verify_base.code != 0:
- # Fallback to origin/base if local doesn't exist
alt_base = f"origin/{actual_base}"
if git(repo_path, "rev-parse", "--verify", "-q", alt_base).code == 0:
actual_base = alt_base
@@ -173,7 +172,6 @@ def analyze_repo_diff(
if len(parts) < 2:
continue
status, file_path = parts[0], parts[1]
- # For rename, parts[1] might contain "old -> new"
if " -> " in file_path:
file_path = file_path.split(" -> ")[1].strip()
@@ -185,14 +183,21 @@ def analyze_repo_diff(
else:
other_files.append(file_path)
- # Get diff for this single file
+ # Determine which files need full diff based on scope
+ files_to_diff: list[str] = []
+ if scope == "contracts":
+ files_to_diff = list(contract_files)
+ elif scope == "all":
+ files_to_diff = list(contract_files) + list(other_files)
+
+ for file_path in files_to_diff:
diff_res = git(repo_path, "diff", f"{actual_base}...HEAD", "--", file_path)
raw_diff = diff_res.stdout
diff_lines = raw_diff.splitlines()
if len(diff_lines) > max_lines_per_file:
truncated_diff = "\n".join(diff_lines[:max_lines_per_file])
- truncated_diff += f"\n... [Dyro Review Pack 提示:该文件改动超过 {max_lines_per_file} 行,已截断以防止 Token 膨胀。共 {len(diff_lines)} 行]"
+ truncated_diff += f"\n... [Dyro 提示:该文件改动超过 {max_lines_per_file} 行,已智能截断以降低 Token 消耗。原文件共 {len(diff_lines)} 行]"
file_diffs[file_path] = truncated_diff
else:
file_diffs[file_path] = raw_diff
@@ -215,6 +220,7 @@ def build_review_pack(
line_id: str,
*,
base: str | None = None,
+ scope: str = "contracts",
run_verify: bool = True,
max_lines_per_file: int = 250,
dry_run: bool = False,
@@ -227,6 +233,7 @@ def build_review_pack(
f"- **开发线 ID**:`{line.id}`",
f"- **分支**:`{line.branch}`",
f"- **对比基线(Base)**:`{base or line.base}`",
+ f"- **审查模式(Scope)**:`{scope}`({'契约与协议靶区高亮·Token优化' if scope == 'contracts' else '全量改动Diff' if scope == 'all' else '改动概览与索引'})",
f"- **生成时间**:{now_str}",
"",
]
@@ -266,6 +273,7 @@ def build_review_pack(
line,
repo_id,
base=base,
+ scope=scope,
max_lines_per_file=max_lines_per_file,
)
if summary is not None:
@@ -303,6 +311,18 @@ def build_review_pack(
if not has_any_contract:
sections.append("> 无显式 DTO / Schema / API 契约文件名变更。\n")
+ has_other_files = any(bool(rs.other_files) for rs in repo_summaries)
+ if has_other_files:
+ sections.append("### 其他业务实现文件变更清单(不展开 Diff 以免消耗大量 Token):")
+ for rs in repo_summaries:
+ if rs.other_files:
+ sections.append(f"**仓 `{rs.repo_id}`({len(rs.other_files)} 个文件)**:")
+ for of in rs.other_files[:60]:
+ sections.append(f"- `{of}`")
+ if len(rs.other_files) > 60:
+ sections.append(f"- ... (省略剩余 {len(rs.other_files) - 60} 个文件)")
+ sections.append("")
+
# 4. Recommended Review Seats
sections.append("## 四、 分席位独立审查建议(阻断上下文二次方膨胀)")
sections.append("> 💡 **审查指南**:不要把整个大包塞进同一个 AI 会话。推荐使用独立对话分别委派以下三个席位,各席位完成审查后向主会话仅汇总结论:\n")
@@ -313,31 +333,32 @@ def build_review_pack(
)
# 5. Compact Code Diffs
- sections.append("## 五、 精简增量代码 Diff(Compact Diff)")
- for rs in repo_summaries:
- if not rs.file_diffs:
- continue
- sections.append(f"### 仓库 `{rs.repo_id}`")
- if rs.commits_log:
- sections.append("最近提交记录(点击展开)
\n")
- sections.append(f"```text\n{rs.commits_log}\n```\n \n")
-
- # Contract diffs first
- if rs.contract_files:
- sections.append(f"#### 1. 契约与协议代码 Diff({len(rs.contract_files)} 个)")
- for cf in rs.contract_files:
- diff_text = rs.file_diffs.get(cf, "")
- if diff_text.strip():
- sections.append(f"**文件:`{cf}`**")
- sections.append(f"```diff\n{diff_text.strip()}\n```\n")
-
- # Other diffs
- if rs.other_files:
- sections.append(f"#### 2. 业务实现代码 Diff({len(rs.other_files)} 个)")
- for of in rs.other_files:
- diff_text = rs.file_diffs.get(of, "")
- if diff_text.strip():
- sections.append(f"{of}(点击展开 Diff)
\n")
- sections.append(f"```diff\n{diff_text.strip()}\n```\n \n")
+ if scope != "summary":
+ sections.append("## 五、 增量代码 Diff(Compact Diff)")
+ for rs in repo_summaries:
+ if not rs.file_diffs:
+ continue
+ sections.append(f"### 仓库 `{rs.repo_id}`")
+ if rs.commits_log:
+ sections.append("最近提交记录(点击展开)
\n")
+ sections.append(f"```text\n{rs.commits_log}\n```\n \n")
+
+ # Contract diffs first
+ if rs.contract_files:
+ sections.append(f"#### 1. 契约与协议代码 Diff({len(rs.contract_files)} 个)")
+ for cf in rs.contract_files:
+ diff_text = rs.file_diffs.get(cf, "")
+ if diff_text.strip():
+ sections.append(f"**文件:`{cf}`**")
+ sections.append(f"```diff\n{diff_text.strip()}\n```\n")
+
+ # Other diffs
+ if scope == "all" and rs.other_files:
+ sections.append(f"#### 2. 业务实现代码 Diff({len(rs.other_files)} 个)")
+ for of in rs.other_files:
+ diff_text = rs.file_diffs.get(of, "")
+ if diff_text.strip():
+ sections.append(f"{of}(点击展开 Diff)
\n")
+ sections.append(f"```diff\n{diff_text.strip()}\n```\n \n")
return "\n".join(sections)
From 04eaa797a7bb819c2244226485640716269af5b8 Mon Sep 17 00:00:00 2001
From: DandreYang <13072547+Dandre126@users.noreply.github.com>
Date: Sun, 13 Sep 2026 20:54:45 +0800
Subject: [PATCH 3/4] =?UTF-8?q?chore(=E5=8F=91=E5=B8=83):=20=E5=87=86?=
=?UTF-8?q?=E5=A4=87=200.7.16?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/dyro/bridge/skill/SKILL.md | 2 +-
tests/test_cli.py | 4 ++--
tests/test_console_artifacts.py | 4 ++--
tests/test_release_gates.py | 2 +-
uv.lock | 2 +-
7 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54618ee..2312476 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
## Unreleased
+## 0.7.16 - 2026-09-13
+
+对使用者的影响:新增开发线增量审查靶区提取与本地门禁核验(`dyro review verify` 与 `dyro review pack`),大幅降低多仓协同中代码审查与 LLM 交互的 Token 消耗与注意力稀释。
+
+- 新增 `dyro review verify --line `:一键自动化运行指定开发线各挂载仓的本地静态门禁,0 Token 拦截编译错误、类型错误与语法测试报错。
+- 新增 `dyro review pack --line [--scope contracts|all|summary] [--out ]`:自动提取跨仓增量 Diff,自动识别高危 Schema/DTO/API 契约文件并建立索引;支持 `--scope contracts`(仅提取核心契约Diff,默认)、`--scope all`(全量Diff带防膨胀截断)与 `--scope summary`(仅改动索引总览),将输入 Token 压缩 90% 以上。
+
## 0.7.15 - 2026-09-12
对使用者的影响:`dyro dispatch` 的异步运行不再因为并发读取运行状态而
diff --git a/pyproject.toml b/pyproject.toml
index 345055f..bbdf50b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "dyro"
-version = "0.7.15"
+version = "0.7.16"
description = "DyroEngineeringFlow: local-first automation and delivery control for multi-repository teams"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/src/dyro/bridge/skill/SKILL.md b/src/dyro/bridge/skill/SKILL.md
index b5df812..cfeb739 100644
--- a/src/dyro/bridge/skill/SKILL.md
+++ b/src/dyro/bridge/skill/SKILL.md
@@ -50,7 +50,7 @@ release, publish, console, install, or any confirmation/approval field.
```json
{
"protocol": {"major": 1, "minor": 0},
- "client": {"name": "dyro-agent-bridge-skill", "version": "0.7.15"},
+ "client": {"name": "dyro-agent-bridge-skill", "version": "0.7.16"},
"operation": "bridge.capabilities.compact",
"input": {}
}
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c3b0174..1fe8e34 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2270,6 +2270,6 @@ def test_package_version_matches_pyproject(self) -> None:
from dyro import __version__
metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
- self.assertEqual(metadata["project"]["version"], "0.7.15")
- self.assertEqual(__version__, "0.7.15")
+ self.assertEqual(metadata["project"]["version"], "0.7.16")
+ self.assertEqual(__version__, "0.7.16")
self.assertEqual(__version__, metadata["project"]["version"])
diff --git a/tests/test_console_artifacts.py b/tests/test_console_artifacts.py
index 6148dae..f3c8180 100644
--- a/tests/test_console_artifacts.py
+++ b/tests/test_console_artifacts.py
@@ -225,12 +225,12 @@ def test_page_and_manifest_keep_p3_fail_closed_pins(self) -> None:
self.assertNotEqual(refresh_at, -1)
self.assertNotIn(b"/artifacts", script.body[refresh_at:next_fn])
- def test_package_version_is_0_7_15(self) -> None:
+ def test_package_version_is_0_7_16(self) -> None:
import tomllib
from pathlib import Path
metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
- self.assertEqual(metadata["project"]["version"], "0.7.15")
+ self.assertEqual(metadata["project"]["version"], "0.7.16")
class ConsoleArtifactServiceTests(WorkspaceCase):
diff --git a/tests/test_release_gates.py b/tests/test_release_gates.py
index cb1007c..e02b04e 100644
--- a/tests/test_release_gates.py
+++ b/tests/test_release_gates.py
@@ -32,7 +32,7 @@ def test_physics_train_refuses_published_0_6_9_tag(self) -> None:
def test_0_7_release_runs_gates_without_claiming_1_0(self) -> None:
stdout = StringIO()
with redirect_stdout(stdout):
- code = main(["--root", str(ROOT), "--release-tag", "v0.7.15"])
+ code = main(["--root", str(ROOT), "--release-tag", "v0.7.16"])
self.assertEqual(code, 0)
self.assertIn("0.7 gates present", stdout.getvalue())
self.assertNotIn("1.0 gates present", stdout.getvalue())
diff --git a/uv.lock b/uv.lock
index 459980c..b7d18cd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -286,7 +286,7 @@ wheels = [
[[package]]
name = "dyro"
-version = "0.7.15"
+version = "0.7.16"
source = { editable = "." }
dependencies = [
{ name = "cryptography" },
From 12c8f87e02e2e8977b6f273472ac1529f39de2ef Mon Sep 17 00:00:00 2001
From: DandreYang <13072547+Dandre126@users.noreply.github.com>
Date: Sun, 13 Sep 2026 20:57:50 +0800
Subject: [PATCH 4/4] =?UTF-8?q?fix(lint):=20=E4=BF=AE=E5=A4=8D=20ruff=20?=
=?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=A1=B9=20(=E6=9C=AA=E4=BD=BF=E7=94=A8?=
=?UTF-8?q?=E5=8F=98=E9=87=8F=E4=B8=8E=E6=97=A0=E5=8D=A0=E4=BD=8D=E7=AC=A6?=
=?UTF-8?q?f-string)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/dyro/cli.py | 2 +-
src/dyro/review_pack.py | 6 ++----
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/dyro/cli.py b/src/dyro/cli.py
index d8a0c0f..9905d32 100644
--- a/src/dyro/cli.py
+++ b/src/dyro/cli.py
@@ -3247,7 +3247,7 @@ def cmd_review_verify(args: argparse.Namespace) -> None:
status_str = "PASS" if r.passed else "FAIL"
print(f"[{status_str}] {r.repo_id}: {' '.join(r.argv)} ({r.elapsed_seconds:.2f}s)")
if not r.passed and r.stdout:
- print(f" --- 失败日志摘录 ---\n " + "\n ".join(r.stdout.splitlines()[-15:]))
+ print(" --- 失败日志摘录 ---\n " + "\n ".join(r.stdout.splitlines()[-15:]))
if failures:
raise DyroError(f"开发线 {line.id} 本地静态门禁未通过({len(failures)}/{len(results)} 失败)")
print(f"\n全部门禁通过(共 {len(results)} 项)。")
diff --git a/src/dyro/review_pack.py b/src/dyro/review_pack.py
index d23d2d5..66ac974 100644
--- a/src/dyro/review_pack.py
+++ b/src/dyro/review_pack.py
@@ -5,10 +5,8 @@
import fnmatch
from pathlib import Path
import time
-from typing import Sequence
from .config import Config
-from .errors import DyroError, ValidationError
from .process import git, run
from .workspace import get_line, line_repository_path, Line
@@ -171,7 +169,7 @@ def analyze_repo_diff(
parts = line_item.split(maxsplit=1)
if len(parts) < 2:
continue
- status, file_path = parts[0], parts[1]
+ _status, file_path = parts[0], parts[1]
if " -> " in file_path:
file_path = file_path.split(" -> ")[1].strip()
@@ -229,7 +227,7 @@ def build_review_pack(
now_str = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
sections: list[str] = [
- f"# Dyro 开发线复核审查靶区包(Review Pack)",
+ "# Dyro 开发线复核审查靶区包(Review Pack)",
f"- **开发线 ID**:`{line.id}`",
f"- **分支**:`{line.branch}`",
f"- **对比基线(Base)**:`{base or line.base}`",