工程(一卡一提交上岸套件): reshape_cards 重塑工具+合回闸数卡三判据+回执 tree 对齐+批末顺序加重塑步(process-conventions §1.2+iter 技能同语)——守卫实测拦 iter40 十八笔未归卡出重塑指令;工具孤立验证两卡+收尾全路径通过;条目完成即删

This commit is contained in:
DKChan committed 2026-10-28 04:00:15 +08:00
1 parent 8f5506f7e3
commit d47a55345b
4 files changed
+210 -10

No files matched your search

+1 -1
View File
@@ -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<N> 形)在跑闸前执行 `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
+199
View File
@@ -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<N> 形分支做数卡校验)。原分支史保留在
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<N> 形)")
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<N> 形——非迭代批免重塑" % 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()
+10 -1
View File
@@ -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,
-8
View File
@@ -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 对指定套件自动做真撤销(回退指定改动→预期红→还原→预期绿)出回执,替代人工三连;断言配额维持现状不设(用户裁)。