预合并审查 是做什么的?
创建链接回PRD和切片问题的GitHub PR,然后对照项目的架构原则审查完整差异。生成咨询性发现——不会阻止合并、自动修复代码或创建问题。
调用位置
这是一个主流水线技能,用于实现验证通过后、合并至主分支之前,或在接手他人PR进行审查时使用。
当分支已准备好创建PR、进行架构审查和最终的计划到代码一致性检查时,使用 /pre-merge。当你审查的不是自己创建的PR时,使用 /pre-merge --pr <number>。
不要将其用作实现验证、QA验收或重构规划的替代品。它假定工作已经构建完毕并准备审查。
模式
/pre-merge 以三种模式之一运行。所有三种模式都重用 review-checklist.md 中定义的架构审查维度;它们的不同之处在于消费什么和产生什么。
- 作者模式(默认)——在你的分支上调用,不带
--pr参数。该技能创建PR(阶段2)并将发现作为咨询性建议打印到终端(阶段4),然后交还控制权——它不会自己进行任何修复提交,也不会开启第二轮审查。 这是/execute步骤6在有人值守路径上自动调用的模式。它读取的内容由分支状态决定,而不是由标志决定: 首次审查读取整个切片,即整个分支,除非切片的工作项记录了起始点;针对已带有审查货币戳记且其头部随后发生移动的PR重新运行时,会将戳记后的提交作为审查的主题,并且不会重新读取之前审查覆盖的内容。阶段1步骤4做出该决定并打印出来。增量是主题,而不是审查者可以读取的限制:review-checklist.md决定了审查者读取的内容,其中包括增量周围的代码树。 - 审查者模式——使用
/pre-merge --pr <number>针对不是你创建的PR进行调用。该技能跳过PR创建(PR已存在),并生成草稿评论文本(阶段4)供你审查和发布,该文本按照references/comment-craft.md(5P门、三重R、评论信号、MMG交换)的结构组织。 - 循环模式——以
/pre-merge --loop方式调用,或在/execute在无人值守运行中交回控制权时自动进入。与作者模式类似地运行阶段1-4,但阶段1步骤4的范围决策除外:循环模式始终读取整个分支,从不获取后戳增量,然后继续进入阶段5:它将每一条发现记录在持久分类账中——记录在PR正文中,或工作项上,位于 references/state-backend.md § 审查记录存放位置所指出的位置——并附加支持或反驳每一条发现的证据,然后交回给操作者。在此模式下,发现不再是终端输出;它们成为带有所有者的持久化行。循环模式不修复任何内容,也不进行任何提交——操作者决定如何处理每个发现(参见阶段5 § 为什么循环不修复)。
循环模式的退出条件是每个发现都有所有者——绝不是审查结果干净。 “零发现”是一个禁止的终止信号,这与阶段4现有的最低发现数量保护一致,该保护的存在是因为在非平凡的差异上出现近乎空的审查意味着审查提前结束,而不是代码完美无瑕。一个在干净审查上终止的循环将可靠地产生干净的审查而别无其他(Meadows,《寻求错误目标》;Leveson:永远不要奖励低事件计数——要奖励报告本身)。
如果你正在用户的工作分支之外的分支上运行,并且没有提供 --pr,请询问一次用户是否意味着针对特定的PR编号使用审查者模式,而不是猜测——自动检测可以节省一次按键,但错误分类模式会产生本应是本地建议的草稿评论,反之亦然。
使用时机
- 作者模式: QA通过后、合并功能分支到主分支之前;在Ralph完成AFK执行并且你已验证行为之后;或者对于任何你想在合并前进行审查的分支,即使没有完整的流水线运行。
- 审查者模式: 当队友或外部贡献者打开一个PR,并且你希望将
review-checklist.md中的完整架构审查应用于他们的差异并生成建设性的评论文本时。 - 循环模式: 当发现必须在产生它们的会话之后仍然存在时——来自
/execute的AFK交接,或者任何你将分多次审查且不想每次都重新推导的分支。在你付出的成本是丢失发现的线索,而不是进行修复时使用它。
执行流程
阶段1:收集上下文
作者模式:
-
询问PRD问题编号。 如果此更改未经过完整流水线,则接受“none”。在从AFK
/execute交接进入循环模式时,不要询问——从交接中获取问题编号(/execute步骤6传递了它),如果缺失则视为“none”。此时无人可以回答,因此在此处的无条件问题会导致运行在其第一步就挂起。在Sidecar后端上,答案是W-<n>工作项ID(步骤2)。此处的“none”意味着阶段2无法在PR上记录内容,并且审查笔记和循环模式分类账没有归属地:references/state-backend.md § 审查记录存放位置说明了它们会发生什么。 -
如果提供了PRD:
gh issue view <number> gh issue list --search "in:body #<prd-number>" --state all --json number,title,state,body --limit 100从每个切片问题的正文中解析边界映射(Produces/Consumes部分)。
在Sidecar后端上(
bash "$SC" backend slices返回sidecar,依据 references/state-backend.md;下面的块会询问它,并在拒绝时停止,而不是回退到gh issue),PRD及其切片是工作项,步骤1中询问的编号是W-<n>ID:此分支实现的切片,由/execute传递,或者其PRD。没有gh issue命令运行;溯源关系和边界映射来自工作项:SC="<此技能目录>/references/sidecar.sh" W="W-<n>" case "$(bash "$SC" backend slices)" in sidecar) ;; github) echo "后端是github:使用上面的问题块读取溯源关系"; exit 0 ;; *) echo "溯源关系中止:sidecar.sh backend slices 失败;不猜测github路径" >&2; exit 1 ;; esac J="$(bash "$SC" work view "$W" --json)" || { echo "溯源关系中止:无法读取 $W" >&2; exit 1; } case "$(jq -r '.kind // empty' <<<"$J")" in slice) PRD="$(jq -r '.parent // empty' <<<"$J")" ;; prd) PRD="$W" ;; *) echo "溯源关系中止:$W 没有 kind" >&2; exit 1 ;; esac SLICES="$W" if [ -n "$PRD" ]; then bash "$SC" work view "$PRD" --json | jq -r '"# \(.id) (prd, \(.state))\n\(.body)"' SLICES="$(bash "$SC" work list --kind slice --parent "$PRD" --state all)" \ || { echo "溯源关系中止:无法列出 $PRD 的切片" >&2; exit 1; } fi while IFS= read -r S; do [ -n "$S" ] || continue bash "$SC" work view "$S" --json | jq -r '"# \(.id) (slice, \(.state))\n\(.body)"' done <<<"$SLICES"从这些正文中解析边界映射,方式与从问题正文解析完全相同。保留
$W:阶段2将PR记录在其上。 -
检测基础分支并评估差异:
SC="<此技能目录>/references/sidecar.sh" # $BASE_REF 是范围端点;$BASE_BRANCH 是 `--base` 接受的名称。 BASE_REF="$(bash "$SC" base-ref)" || { echo "base-ref 失败:不猜测基础分支" >&2; exit 1; } BASE_BRANCH="$(bash "$SC" base-ref --name)" || { echo "base-ref --name 失败:不猜测基础分支" >&2; exit 1; } git diff "$BASE_REF...HEAD" --stat git log --oneline "$BASE_REF..HEAD"base-ref在有声明的base_ref时打印它,否则打印默认的origin/HEAD。在非零退出时不会打印任何引用:停止并传递其标准错误,而不是猜测基础分支。对于堆叠PR切片,此处设置的变量不会到达后续块:将兄弟切片的分支名称(PR将目标的上游分支)放入每个消费该名称的块的BASE_BRANCH=占位符中(你的后端的阶段2块和阶段4戳记块)。如果与基础分支没有差异,则告诉用户没有需要审查的内容并停止。残留:
$BASE_REF仅在最近一次git fetch后才是最新的。当origin/<base>无法解析时,base-ref会针对本地分支进行测量并在标准错误上说明;如果计数看起来不正确,执行git fetch并重新运行。 -
决定范围——整个切片,还是后戳增量。 此决策仅适用于作者模式。循环模式跳过它,并无条件地将
SCOPE_FROM设置为$BASE_REF,根据阶段3的循环模式说明,每次调用时读取整个分支。首次审查读取整个切片:sidecar.sh range给出其左端点,当步骤2命名的工作项在分支上仍有记录时,左端点是切片的记录起始点,否则是$BASE_REF,因此在github后端上,切片是整个分支。针对此技能已经戳记过的PR(其头部随后发生了移动)重新运行——一个/fix-findings轮次,或故意删除锁定后的人工修复——会将后戳增量作为其主题。戳记位于backend stamp所指示的位置,依据 references/state-backend.md:PR正文或Sidecar。使用与/closeout步骤2相同的表达式读取PR正文;scripts/test-review-currency-marker.sh固定了两个字节相同的戳记,并且scripts/test-delta-review-scope.sh针对其命名的每个分支状态在两个后端上运行此块与一个夹具:SC="<此技能目录>/references/sidecar.sh" case "$(bash "$SC" backend stamp)" in sidecar) REVIEWED_SHA=$(bash "$SC" stamp read "$(git branch --show-current)") ;; github) REVIEWED_SHA=$(gh pr list --head "$(git branch --show-current)" --json body -q '.[0].body // ""' \ | sed -n 's/.*<!-- reviewed-at: \([0-9a-f]\{40\}\) -->.*/\1/p' | tail -1) ;; *) echo "范围中止:sidecar.sh backend stamp 失败;不猜测github路径" >&2; exit 1 ;; esac W="<步骤2命名的 W-<n>,在github后端上为空>" BASE_REF="$(bash "$SC" base-ref)" || { echo "范围中止:sidecar.sh base-ref 失败" >&2; exit 1; } RANGE="$(bash "$SC" range ${W:+"$W"})" || { echo "范围中止:sidecar.sh range 失败" >&2; exit 1; } SLICE_FROM="${RANGE%%..*}" SCOPE_FROM="$SLICE_FROM" if [ -n "$REVIEWED_SHA" ] \ && git merge-base --is-ancestor "$REVIEWED_SHA" HEAD 2>/dev/null \ && [ "$REVIEWED_SHA" != "$(git rev-parse HEAD)" ] \ && [ -z "$(git log --merges --format=%H "$REVIEWED_SHA..HEAD")" ]; then SCOPE_FROM="$REVIEWED_SHA" elif [ -n "$REVIEWED_SHA" ] \ && ! git merge-base --is-ancestor "$REVIEWED_SHA" HEAD 2>/dev/null \ && [ "$(git rev-parse -q --verify "$REVIEWED_SHA^{tree}")" = "$(git rev-parse 'HEAD^{tree}')" ]; then SCOPE_FROM="HEAD" fi if [ "$SCOPE_FROM" = HEAD ]; then echo "审查范围:无,仅历史重写——HEAD 携带在 $REVIEWED_SHA 审查过的代码树" elif [ "$SCOPE_FROM" = "$BASE_REF" ]; then echo "审查范围:整个分支,从 $BASE_REF 开始" elif [ "$SCOPE_FROM" = "$SLICE_FROM" ]; then echo "审查范围:整个切片,从 $SLICE_FROM($W 的记录起始点)开始" else echo "审查范围:后戳增量,从 $SCOPE_FROM 开始" fi git diff "$SCOPE_FROM...HEAD" --stat git log --oneline "$SCOPE_FROM..HEAD"四种状态会回退到整个切片,每种都是有目的的:没有戳记(首次审查,或手动创建的PR);戳记不是
HEAD的祖先且HEAD不再携带其代码树(分支被重写且内容随之改变,或者被审查的提交已消失——/closeout将此称为更强的分歧信号,并且它不是更小的审查);戳记等于HEAD(没有新的内容可以限定范围);以及戳记之后的合并提交(后戳增量将携带基础分支自身的更改,而这些更改不是该分支上任何人编写的)。除非range找到了记录起始点,否则整个切片就是整个分支,当range找到一个因折叠或变基而被重写掉的记录起始点时,它会在标准错误上说明并回答$BASE_REF,而不会重新锚定起始点。该决策会被打印出来,以便终端读取和PR对读取的内容说同样的话。保留审查过代码树的重写并非分歧。 当被戳记的提交不是
HEAD的祖先但仍然存在于本地并且携带与HEAD相同的代码树时,历史发生了变化而内容没有:阶段4的交接所提供的折叠(git rebase -i --autosquash)正是产生这种情况。该块打印review scope: none并将SCOPE_FROM设置为"HEAD"。跳过阶段3以及阶段4中除戳记(在HEAD处重新戳记)和交接菜单之外的所有内容。戳记证明审查过的内容,而代码树哈希就是那个确切的内容。如果被戳记的提交在本地不可用,则无法比较代码树,重写将采用上述的整个分支回退。在增量重新运行时,增量是主题,并且代码树仍然可读。 向每个阶段3审查者简要介绍后戳提交声称关闭的发现——本次会话中之前的输出,循环模式写入的分类账(位于 references/state-backend.md § 审查记录存放位置所指出的位置),或提交消息本身——以及任何存在的
/fix-findings打断器裁决,以及bash "$SC" profile输出的逐字内容,这些是阶段3的契约授予每个审查者的。每个提交的第一个问题是修复是否关闭了其发现所命名的每个站点的发现,这是修复完整性维度;第二个问题是…
(文档已截断以供翻译)