Reject rewritten target history during local task recovery

Why: A LOCAL_READY receipt must permit normal target advancement without accepting a rewind or divergent target history after its observation.

Evidence: Two disposable real Git regressions reproduced erroneous successful recovery before the guard; all 18 task recovery tests pass afterward. Raw SELF receipts are in the task card.

Impact: Reject abnormal target ancestry before receipt import; preserve normal local recovery and strict delivery defaults. Correct stale implementation-stage documentation without activating the queue.

Refs: Documents implementation plan ba4044d U2; independent review R1 and R2; .task/feature-merge-queue-policy-draft.md
This commit is contained in:
2026-10-13 08:44:02 +00:00
parent 7f197e9e71
commit cca59c88dd
4 changed files with 332 additions and 8 deletions
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,7 +1,7 @@
# 最终验证与合入调度(候选条文 r1)
状态:approved(r1 条文作为实施计划基线);执行模式:未切换;日期:2026-10-13(Asia/Shanghai);规范采纳负责人:weichen。
本聊天在 r1 草案交付后收到用户「继续」,按上一轮出口进入实施计划编制;r1 来源提交为 913b214f2826a50b3dab65f79d9ebf83e84f2519。实施与测试计划仍须阶段 5 评审,规范批准不等于模式已启用。
r1 条文来源提交为 913b214f2826a50b3dab65f79d9ebf83e84f2519;Documents 实施与测试计划 r1@ba4044d57e38d0887dd425d052abf26b20a28bf4 已于 2026-10-13 获用户「批准」。阶段 6 的本地实现产物仍待人工确认,规范及计划批准不等于模式已启用。
现行规则仍以根 AGENTS.md、[迭代交付](delivery.md)、[开发门禁](governance.md) 和已采纳 gitlink 为准。
本分支已实施 CLI 同步/恢复及源码外证据基础,自检依据见本任务卡;线上队列、CI 分流、平台保护和当前 gitlink 尚未切换,不承诺自动调度能力已就位。
@@ -134,5 +134,5 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
| MQ8 | 平台拒绝未满足准入者,合法固定候选可合入;manual 能力如实登记,自动接管另验证旧代次拒绝 | NOT_RUN(所有者操作后验证) |
| MQ9 | 文档链接、入口、版本、单一来源、状态与模板字段一致 | 本轮执行;原始输出见任务卡 SELF 收据 |
这些验收 ID 同时用于后续实施追溯;本轮只交付规范文本,不把未来判据写成已通过。
这些验收 ID 用于实施与验收追溯;本分支已完成可独立实施的 CLI/证据基础、规则与模板准备及临时真实 Git 场景自检。固定规范采纳、实际模式切换、平台正负例和真实 CI/三任务试行仍待各自门禁,未执行项保持 NOT_RUN;当前进展与原始证据统一见任务卡。
+5
View File
@@ -266,6 +266,11 @@ def validate_precheck(receipt, purpose="start"):
for obj in (receipt["before"], after)):
raise EvidenceError("Precheck belongs to a different task branch")
git("merge-base", "--is-ancestor", context["r0"], after["target_head"])
if purpose == "resume-local":
try:
git("merge-base", "--is-ancestor", after["target_head"], current["target_head"])
except EvidenceError:
raise EvidenceError("Precheck target history has rewound or diverged") from None
def find_card():
+45
View File
@@ -115,6 +115,51 @@ class TaskRecoveryTests(unittest.TestCase):
current = f.git(self.wt, "rev-parse", "origin/main").stdout.strip()
self.assertNotEqual(json.loads(original)["after"]["target_head"], current)
def assert_history_change_rejected(self, receipt):
f = self.fixture
before = self.card.read_bytes()
original = receipt.read_bytes()
head = f.git(self.wt, "rev-parse", "HEAD").stdout
result = self.resume(receipt)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("Precheck target history has rewound or diverged", result.stdout)
self.assertNotIn("[resumed]", result.stdout)
self.assertEqual(self.card.read_bytes(), before)
self.assertEqual(receipt.read_bytes(), original)
self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head)
def test_local_resume_rejects_target_rewind_before_import(self):
f = self.fixture
r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip()
f.advance_remote()
receipt = self.capture()
observed = json.loads(receipt.read_text())["after"]["target_head"]
self.assertNotEqual(observed, r0)
# Only this test's disposable bare remote is rewritten.
f.git(f.remote, "update-ref", "refs/heads/main", r0, observed)
# Reproduce recovery after another client operation refreshed the target.
f.git(self.wt, "fetch", "origin", "+refs/heads/main:refs/remotes/origin/main")
self.assert_history_change_rejected(receipt)
def test_local_resume_rejects_divergent_target_before_import(self):
f = self.fixture
r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip()
f.advance_remote()
receipt = self.capture()
observed = json.loads(receipt.read_text())["after"]["target_head"]
f.git(f.repo, "switch", "--detach", r0)
(f.repo / "divergent-main").write_text("different target history\n")
f.git(f.repo, "add", "divergent-main")
f.git(f.repo, "commit", "-m", "Create divergent fixture history")
divergent = f.git(f.repo, "rev-parse", "HEAD").stdout.strip()
f.git(f.repo, "push", "origin", "HEAD:refs/heads/divergent")
self.assertEqual(f.git(f.repo, "merge-base", "--is-ancestor", observed,
divergent, check=False).returncode, 1)
# Both targets retain R0; only normal ancestry may permit recovery.
f.git(f.remote, "update-ref", "refs/heads/main", divergent, observed)
f.git(self.wt, "fetch", "origin", "+refs/heads/main:refs/remotes/origin/main")
self.assert_history_change_rejected(receipt)
def test_make_resume_exports_the_selected_mode_and_receipt(self):
f = self.fixture
receipt = self.capture()