From d47a55345be96cf53f6929e20692392237a53f38 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 04:00:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E4=B8=80=E5=8D=A1=E4=B8=80?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=B8=8A=E5=B2=B8=E5=A5=97=E4=BB=B6):=20resh?= =?UTF-8?q?ape=5Fcards=20=E9=87=8D=E5=A1=91=E5=B7=A5=E5=85=B7=EF=BC=8B?= =?UTF-8?q?=E5=90=88=E5=9B=9E=E9=97=B8=E6=95=B0=E5=8D=A1=E4=B8=89=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=EF=BC=8B=E5=9B=9E=E6=89=A7=20tree=20=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=EF=BC=8B=E6=89=B9=E6=9C=AB=E9=A1=BA=E5=BA=8F=E5=8A=A0?= =?UTF-8?q?=E9=87=8D=E5=A1=91=E6=AD=A5(process-conventions=20=C2=A71.2?= =?UTF-8?q?=EF=BC=8Biter=20=E6=8A=80=E8=83=BD=E5=90=8C=E8=AF=AD)=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=AE=88=E5=8D=AB=E5=AE=9E=E6=B5=8B=E6=8B=A6=20iter40?= =?UTF-8?q?=20=E5=8D=81=E5=85=AB=E7=AC=94=E6=9C=AA=E5=BD=92=E5=8D=A1?= =?UTF-8?q?=E5=87=BA=E9=87=8D=E5=A1=91=E6=8C=87=E4=BB=A4;=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=AD=A4=E7=AB=8B=E9=AA=8C=E8=AF=81=E4=B8=A4=E5=8D=A1?= =?UTF-8?q?=EF=BC=8B=E6=94=B6=E5=B0=BE=E5=85=A8=E8=B7=AF=E5=BE=84=E9=80=9A?= =?UTF-8?q?=E8=BF=87;=E6=9D=A1=E7=9B=AE=E5=AE=8C=E6=88=90=E5=8D=B3?= =?UTF-8?q?=E5=88=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/engineering/process-conventions.md | 2 +- tools/reshape_cards.py | 199 ++++++++++++++++++++++ tools/run_all_gates.sh | 11 +- 待办清单.md | 8 - 4 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 tools/reshape_cards.py diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 06bb9523..84825cfc 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -10,7 +10,7 @@ 改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一): 1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运);建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。迭代批开工细则见 §3 与 iter 技能 -2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑),然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`,再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑 +2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 重塑 → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑);**重塑**=迭代分支(iter 形)在跑闸前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`(合回闸对迭代分支数卡校验,未归卡提交拦下),再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑 3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制 4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent diff --git a/tools/reshape_cards.py b/tools/reshape_cards.py new file mode 100644 index 00000000..72f609fd --- /dev/null +++ b/tools/reshape_cards.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""一卡一提交上岸套件的重塑工具:把迭代分支的线性提交史按卡末态快照压成一卡一笔。 + +分支内自由提交(T 段、修复、文书随做随提),master 台账只收每张卡一笔提交(可选追加 +一笔纯文档收尾)。本工具在批末「并 master 之后、跑闸之前」执行:以每张卡最后一笔提交 +的文件树建新提交、逐卡链在 master 尖上;自带「重塑前后树逐字节一致」断言与 backup +分支,重塑只改账面、不改变任何文件内容。 + +用法: + python tools/reshape_cards.py --branch iter40 \\ + --card 卡1=<该卡最后一笔提交> --card 卡2=<...> [--tail <收尾最后一笔>] [--dry-run] + 可选 --msg 卡1="一行结论"(多条)补提交说明;缺省说明=卡号本身。 + +规则: + 1. master 尖必须是分支祖先(批末先并 master;不满足即拒绝,防快照回退 master 内容)。 + 2. 各卡边界必须是分支第一-parent 链上的提交、按链序递增;最后一笔卡边界或收尾边界 + 必须恰为分支尖——否则存在未归卡提交,拒绝执行并列出。 + 3. 收尾段的改动必须全是文档面(game/ 下非 .md 之外的文件);含代码面先归卡。 + 4. 分支工作区必须干净(脏改动会被快照丢弃,拒绝执行)。 + +非迭代工程小批免重塑(合回闸只对 iter 形分支做数卡校验)。原分支史保留在 +backup/<分支名>-<时间戳> 分支上,可随时回溯。 +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +import time +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + + +def run(args: list[str], check: bool = True, inp: str | None = None) -> str: + r = subprocess.run( + ["git", "-C", str(REPO)] + args, + capture_output=True, text=True, encoding="utf-8", errors="replace", + input=inp, timeout=60, + ) + if check and r.returncode != 0: + sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip())) + sys.exit(1) + return (r.stdout or "").strip() + + +def is_doc_face(p: str) -> bool: + # 代码面=game/ 下非 .md 的行为载体;收尾段只许文档面。 + return not (p.startswith("game/") and not p.lower().endswith(".md")) + + +def worktree_of(branch: str) -> str | None: + wl = run(["worktree", "list", "--porcelain"]) + for block in wl.split("\n\n"): + if ("branch refs/heads/%s" % branch) in block: + for line in block.splitlines(): + if line.startswith("worktree "): + return line[9:].strip() + return None + + +def die(msg: str) -> None: + sys.stderr.write("[重塑] 拒绝执行——%s\n" % msg) + sys.exit(1) + + +def main() -> None: + ap = argparse.ArgumentParser(description="按卡末态快照把迭代分支重塑成一卡一笔") + ap.add_argument("--branch", required=True, help="迭代分支名(iter 形)") + ap.add_argument("--card", action="append", required=True, metavar="卡1=<提交>", + help="该卡在分支上的最后一笔提交,按卡序逐卡给出") + ap.add_argument("--tail", metavar="<提交>", default=None, + help="可选收尾段(纯文档面)的最后一笔提交") + ap.add_argument("--msg", action="append", default=[], metavar='卡1="一行结论"', + help="各卡/收尾的提交说明,可多条") + ap.add_argument("--dry-run", action="store_true", help="只打印重塑计划,不动任何引用") + args = ap.parse_args() + + try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + + branch = args.branch + if not re.fullmatch(r"iter\d+", branch): + die("分支名 %s 不是 iter 形——非迭代批免重塑" % branch) + + master_tip = run(["rev-parse", "master"]) + old_tip = run(["rev-parse", branch]) + rc = subprocess.run(["git", "-C", str(REPO), "merge-base", "--is-ancestor", + master_tip, old_tip], capture_output=True).returncode + if rc != 0: + die("master 尖 %s 不是分支 %s 的祖先——先回分支并 master(git merge master)再重塑," + "否则快照会把 master 新内容回退掉" % (master_tip[:8], branch)) + + wt = worktree_of(branch) + if wt: + st = run(["-C", wt, "-c", "core.quotepath=false", "status", "--porcelain"]) + if st: + die("分支 %s 的工作区(%s)有未提交改动——快照取自提交树,脏改动会被丢弃;" + "先提交或储藏再重塑\n%s" % (branch, wt, st)) + # 分支未检出时引用移动不影响任何工作区,无需检查脏树 + + base = run(["merge-base", "master", branch]) + chain = [x for x in run(["rev-list", "--first-parent", "%s..%s" % (base, branch)]).splitlines() if x] + chain.reverse() # 老 → 新 + if not chain: + die("分支 %s 相对 master 无自有提交,无可重塑" % branch) + idx = {sha: i for i, sha in enumerate(chain)} + + msgs: dict[str, str] = {} + for m in args.msg: + k, _, v = m.partition("=") + msgs[k.strip()] = v.strip() + + cards: list[tuple[str, int]] = [] + for c in args.card: + label, _, sha = c.partition("=") + full = run(["rev-parse", sha.strip()]) + if full not in idx: + die("卡边界 %s(%s)不在分支 %s 相对 master 的第一-parent 链上" % (label, sha, branch)) + cards.append((label.strip(), idx[full])) + cards.sort(key=lambda x: x[1]) + if len({i for _, i in cards}) != len(cards): + die("卡边界重复指向同一笔提交") + + tail_i: int | None = None + if args.tail: + full = run(["rev-parse", args.tail.strip()]) + if full not in idx: + die("收尾边界 %s 不在分支第一-parent 链上" % args.tail) + tail_i = idx[full] + if cards and tail_i <= cards[-1][1]: + die("收尾边界必须排在最后一笔卡边界之后") + + last = tail_i if tail_i is not None else (cards[-1][1] if cards else -1) + if last != len(chain) - 1: + rest = chain[last + 1:] + die("最后一笔边界之后还有 %d 笔未归卡提交——归入所属卡或收尾后重试:\n %s" + % (len(rest), "\n ".join(run(["log", "--no-walk", "--format=%h %s", s]) for s in rest[:8]))) + + # 收尾段只许文档面 + if tail_i is not None: + prev = chain[cards[-1][1]] if cards else base + files = run(["-c", "core.quotepath=false", "diff", "--name-only", prev, chain[tail_i]]).splitlines() + bad = [f for f in files if not is_doc_face(f.strip())] + if bad: + die("收尾段含代码面改动(应归卡):\n " + "\n ".join(bad[:10])) + + # 建新链:master 尖起,逐卡快照 + plan: list[tuple[str, str, str]] = [] # (说明, 边界 sha, 新提交占位) + segs: list[tuple[str, int]] = cards + ([("收尾", tail_i)] if tail_i is not None else []) + parent = master_tip + new_shas: list[str] = [] + n_match = re.fullmatch(r"iter(\d+)", branch) + itn = n_match.group(1) if n_match else branch + for k, (label, i) in enumerate(segs, start=1): + boundary = chain[i] + tree = run(["rev-parse", "%s^{tree}" % boundary]) + if label == "收尾": + subject = "iter%s-收尾" % itn + else: + subject = "iter%s-卡%d" % (itn, k) + if label in msgs: + subject += ":" + msgs[label] + cp = run(["commit-tree", tree, "-p", parent], inp=subject + "\n") + new_shas.append(cp) + parent = cp + plan.append((subject, boundary, cp)) + new_tip = parent + + diff = run(["diff", "--stat", old_tip, new_tip]) + if diff: + die("安全断言失败:重塑前后树不一致(不应发生,请带输出报缺陷)\n%s" % diff) + + print("[重塑] 计划(分支 %s,原尖 %s):" % (branch, old_tip[:8])) + for subject, boundary, cp in plan: + print(" %s ← %s" % (subject, run(["log", "--no-walk", "--format=%h %s", boundary])[:70])) + print("[重塑] 树一致断言通过;backup 分支将指向原尖 %s" % old_tip[:8]) + if args.dry_run: + print("[重塑] --dry-run:未动任何引用。") + return + + backup = "backup/%s-%s" % (branch, time.strftime("%Y%m%d-%H%M%S")) + run(["branch", backup, old_tip]) + if wt: + run(["-C", wt, "reset", "--hard", new_tip]) + else: + run(["update-ref", "refs/heads/%s" % branch, new_tip]) + print("[重塑] 完成:分支 %s → %s(%d 笔卡提交);原史留 %s" + % (branch, new_tip[:8], len(cards), backup)) + print("[重塑] 下一步:按本批改动域跑 tools/run_all_gates.sh,全绿后主工作区合回。") + + +if __name__ == "__main__": + main() diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 9171e07c..1ace0fc5 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -3,7 +3,7 @@ # 闸清单的唯一定义处。文档只写「收口跑本脚本」,禁在文档里另列闸件清单(那正是 iter27 # 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。 # -# 跑完写回执 game/tests/.gates/receipt.json(commit + 各闸退出码 + 存量基线比对 + 时间戳); +# 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳); # .zcode/hooks/worktree-guard.sh 在主工作区 git merge 时校验本回执,缺或红即拦截。 # # 本脚本是合回前的终局动作:跑之前先冻结(代码与断言不再改)并把 master 并入本分支;每批只跑 @@ -304,6 +304,14 @@ def head(): except Exception: return "unknown" +def tree_hash(): + # 内容树哈希:合回闸按树对齐判定「合回内容=跑闸内容」(重塑/合并会改写 commit 号) + try: + return subprocess.run(["git", "-C", repo, "rev-parse", "HEAD^{tree}"], + capture_output=True, text=True, timeout=5).stdout.strip() + except Exception: + return "unknown" + def fingerprint(exit_code, log): """红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声。 绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。""" @@ -370,6 +378,7 @@ with open(receipt_path, "w", encoding="utf-8") as f: json.dump({ "commit": head(), "commit_short": head()[:8], + "tree": tree_hash(), "timestamp": subprocess.run(["date", "-Iseconds"], capture_output=True, text=True).stdout.strip(), "quick_mode": quick == "1", "domain": domain, diff --git a/待办清单.md b/待办清单.md index 53d7a51a..6f1797e6 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,14 +2,6 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 一卡一提交上岸套件——分支重塑工具+合回闸数卡判据+回执树哈希对齐(2026-10-28 登记,来源=会话提及) - -**实况**:证据=迭代 39/40 两个实现会话 44 笔提交中非实装 27 笔(git log 实测),碎片提交与收口文书直接上岸 master 是最大宗。2026-10-28 用户裁落地形态=重塑方案:分支内自由提交、批末 `tools/reshape_cards.py` 按卡末态快照重塑为一卡一笔(可选一笔文档收尾)、合回闸校验卡数与提交格式、回执以内容树哈希对齐(重塑与合并改写 commit 号后仍可判定「合回内容=跑闸内容」)。批末顺序改为「并 master → 重塑 → 跑闸 → 合回」。落点:tools/reshape_cards.py(新建)+tools/run_all_gates.sh(回执加 tree 字段)+.zcode/hooks/worktree-guard.sh(数卡/无杂笔/格式三判据+树对齐)+design/engineering/process-conventions.md §1.2(批末顺序与上岸形态)+.zcode/skills/iter/SKILL.md(同语)。工程改动(工具代码+文档),用户已发令(2026-10-28 四批全做裁决)。 - -**改动面**:工具代码+文档 - -**状态**:待执行 - #### 撤销证明工具化——undo_proof 自动红绿回执+变异禁令落 test-conventions(2026-10-28 登记,来源=会话提及) **实况**:证据=迭代 40 卡① 十例取证人工三连(绿→真撤销复红→还原复绿)且撞 2026-10-24 变异禁令——禁令原文只在 req-card 技能内,test-conventions 指针空挂(iter40 反哺点 B3 在册)。2026-10-28 用户裁工具化:tools/undo_proof.py 对指定套件自动做真撤销(回退指定改动→预期红→还原→预期绿)出回执,替代人工三连;断言配额维持现状不设(用户裁)。