From 155251fa57f33f1df8f9bfaaa7360ad5a26a01ba Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sun, 2 Aug 2026 21:18:36 +0800 Subject: [PATCH 1/4] fix(planning): pin WP0 validation to immutable merge evidence PR #414 was squash-merged after PR #412, so comparing the original SDD baseline with the moving main branch made the WP0 artifact gate absorb unrelated storage and Raft paths. Record immutable PR and squash-merge evidence, keep WP0 at implemented while exact-main verification is pending, and validate the historical merge-parent diff independently of the current work package. Add regression coverage for concurrent merges, invalid refs, ancestry, and premature lifecycle promotion. Constraint: Keep WP1 and Issue #415 blocked until WP0 has passed exact-main evidence. Confidence: high Scope-risk: narrow Tested: Windows and Ubuntu WSL SDD self-test, validator, Python compile, and git diff checks. Not-tested: GitHub Actions on the pushed Head. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .planning/SDD.md | 62 ++++--- scripts/validate_sdd.py | 379 +++++++++++++++++++++++++++++++++++----- 2 files changed, 379 insertions(+), 62 deletions(-) diff --git a/.planning/SDD.md b/.planning/SDD.md index 0f4482ef..aa8d3d6e 100644 --- a/.planning/SDD.md +++ b/.planning/SDD.md @@ -3,23 +3,31 @@ document: kiwi-sdd title: Kiwi 架构设计与 SDD 开发计划 status: accepted-design authority: sole-project-entry -version: 1 +version: 2 updated_at: 2026-08-02 baseline_repository: arana-db/kiwi baseline_branch: main -baseline_ref: 0c4795ec716299598686fc7c5e0fac03a30e044d -github_snapshot_at: 2026-08-02T07:43:23Z +baseline_ref: 9820162ebdf2d26aa6349e704efe8737b2e73e4a +wp0_pr_number: 414 +wp0_pr_base_ref: 0c4795ec716299598686fc7c5e0fac03a30e044d +wp0_pr_head_ref: e2bfc7deb481590a757f0034874b7f21a4a31aa2 +wp0_merge_parent_ref: cbcbadc27068634d851ab0ed63989d2214ab2408 +wp0_merge_ref: 9820162ebdf2d26aa6349e704efe8737b2e73e4a +wp0_exact_main_verification_ref: none +wp0_exact_main_verification_run: none +wp0_exact_main_verification_status: pending +github_snapshot_at: 2026-08-02T12:57:50Z redis_oracle_tag: 8.8.1 redis_oracle_ref: 77b6c308396c9700672390a210143a8496fb4b10 required_runtime_mode: cache-off executable_scope: M0-M6 long_term_scope: M0-M10 current_work_package: WP0 -current_work_package_status: in-progress +current_work_package_status: implemented current_plan: .planning/SDD.md#wp0 current_issue: 413 current_pr: 414 -next_safe_action: push-pr-link-and-wait-for-final-head-checks +next_safe_action: repair-post-merge-validation-under-416-then-verify-wp0 --- # Kiwi 架构设计与 SDD 开发计划 @@ -154,18 +162,20 @@ M7 到 M10 进入长期架构,但不进入当前实现: | D | Discussion、RFC、Proposal | 设计输入,必须显式采纳 | | E | 历史计划、旧周报、过期 Issue、草稿 | 背景,不形成当前任务 | -### 3.2 2026-08-02T07:43:23Z 实时快照 +### 3.2 2026-08-02T12:57:50Z 实时快照 - Repository:arana-db/kiwi。 - Default branch:main。 -- main:0c4795ec716299598686fc7c5e0fac03a30e044d。 -- Open Issues:63,其中 #413 是本轮创建的 WP0 Primary Issue。 +- main:9820162ebdf2d26aa6349e704efe8737b2e73e4a。 +- Open Issues:63;#415 是 WP1 Oracle provenance 实施 Issue,#416 是 WP0 合并后验证修复 Issue。 - Discussions:17,其中 1 个关闭,没有 accepted answer。 - PR #409:MERGED,对应 merge commit 0c4795ec716299598686fc7c5e0fac03a30e044d。 - Issue #407:CLOSED,关闭时间 2026-08-02T04:44:40Z。 -- PR #412:OPEN,Head 2d5680b5245478637bea31a39e83e7e0be488bca,MERGEABLE/CLEAN,快照时可见检查成功;未合并前 #143 仍视为未完成。 -- PR #356:OPEN,Head f3af349247d02f70ef3adbdd2abd9c03ceb3b870,MERGEABLE/UNSTABLE,macOS Clippy 失败且其余多项检查仍在运行;VectorSet 不进入当前主线。 -- PR #261:OPEN,CONFLICTING/DIRTY,属于长期滞留 PR。 +- PR #412:MERGED,Head 9d1f83360eb52ed23b48b2e5cb1159c93e26e7af,merge commit cbcbadc27068634d851ab0ed63989d2214ab2408;Issue #143 已关闭。 +- PR #414:MERGED,base 0c4795ec716299598686fc7c5e0fac03a30e044d,Head e2bfc7deb481590a757f0034874b7f21a4a31aa2,merge commit 9820162ebdf2d26aa6349e704efe8737b2e73e4a;Issue #413 已关闭。 +- main@9820162ebdf2d26aa6349e704efe8737b2e73e4a 的 push CI run 30747510551 已完成但失败;唯一失败项是 planning SDD validation,其使用旧 baseline 累计了先合并的 PR #412 路径。 +- PR #356:OPEN,Head f1f8d7539812efd4c6c5e99d540744976d4f6261,CONFLICTING/DIRTY,Windows build 和 LeakSanitizer 检查失败;VectorSet 不进入当前主线。 +- PR #261:OPEN,Head fb092812234a54ad3757d35a62ad033136e422c7,GitHub 当前返回 mergeable/mergeStateStatus UNKNOWN,属于长期滞留 PR。 该快照只用于编制本版本。开始任一工作包、创建 PR、复审或验收前必须重新查询实时状态。 @@ -859,7 +869,7 @@ Acceptance criteria;缺少任一字段时,工作包不得进入 ready。 ### WP0:单一 SDD 控制面与事实对账 -状态:in-progress。 +状态:implemented。 目标: @@ -901,14 +911,24 @@ Parent / Related:N/A。 Implementation PR:[#414](https://github.com/arana-db/kiwi/pull/414)。 +Post-merge validation repair:[#416](https://github.com/arana-db/kiwi/issues/416)。 + +合并证据: + +- PR 固定区间:0c4795ec716299598686fc7c5e0fac03a30e044d..e2bfc7deb481590a757f0034874b7f21a4a31aa2; +- merge 固定区间:cbcbadc27068634d851ab0ed63989d2214ab2408..9820162ebdf2d26aa6349e704efe8737b2e73e4a; +- PR #414 于 2026-08-02T12:17:13Z 合并,Issue #413 随后关闭; +- main push CI run 30747510551 / job 91495496924 失败,原因是旧 baseline_ref 把先合并的 PR #412 的 7 个源码路径计入 WP0,而不是 WP0 产物本身失败。 +- WP0 exact-main 成功验证仍为 pending,尚无可写入的成功 ref 或 Actions run。 + Requirement: - REQ-WORK-001 至 REQ-WORK-007。 依赖: -- main@0c4795ec716299598686fc7c5e0fac03a30e044d 的源码事实基线; -- 2026-08-02T07:43:23Z 的 GitHub 快照; +- PR base main@0c4795ec716299598686fc7c5e0fac03a30e044d 的源码事实基线; +- 2026-08-02T12:57:50Z 的 GitHub 快照; - 无前置工作包;本节按 planning-only bootstrap 例外同时作为 WP0 plan。 非目标: @@ -916,7 +936,7 @@ Requirement: - 不修改 Runtime、Storage、Raft、协议、构建或测试行为; - 不实现 WP1-WP7; - 不实现未来的自动 PR traceability checker; -- 不 merge 本 PR,也不以 WP0 自动授权后续源码工作。 +- 不以 PR #414 已合并或 Issue #413 已关闭自动授权后续源码工作;只有合并后 exact-main 验证通过才能继续进入 verified/accepted。 退出门禁: @@ -944,6 +964,7 @@ Primary Issue:#325。 Related: - #315; +- #415(M1-001-T2 Oracle provenance); - OQ-3; - OQ-10。 @@ -1386,7 +1407,7 @@ Requirement: #### Block Cache - Issue #143。 -- PR #412 仍 OPEN;合并前不得标记完成。 +- PR #412 已于 2026-08-02 合并为 cbcbadc27068634d851ab0ed63989d2214ab2408,Issue #143 已关闭;其能力仍按共享预算、全实例/全 CF、table options、指标和基准证据评估。 - 只允许在共享预算、全实例/全 CF、table options、指标和基准均闭合后 accepted。 #### Build Performance @@ -1578,18 +1599,19 @@ docs/sdd/WP-N/ | 字段 | 当前值 | |---|---| -| Baseline | main@0c4795ec716299598686fc7c5e0fac03a30e044d | +| Baseline | main@9820162ebdf2d26aa6349e704efe8737b2e73e4a | | Current milestone | M0 | | Current work package | WP0 | -| Status | in-progress | +| Status | implemented | | Current plan | [.planning/SDD.md 的 WP0 章节](#wp0) | | Current Issue | [#413](https://github.com/arana-db/kiwi/issues/413) | | Current PR | [#414](https://github.com/arana-db/kiwi/pull/414) | +| WP0 exact-main verification | pending | | Required mode | Cache OFF | | M7/M8 | frozen | -| Next safe action | push PR 链接回写,确认 final Head 后等待 required checks 与 review | +| Next safe action | 完成 #416 的固定提交区间验证修复;合并后在 exact main 重跑门禁,WP0 进入 verified/accepted 后才恢复 #415 | -当前规划工作位于隔离分支 codex/kiwi-sdd-architecture,Issue #413 与 PR #414 已建立追踪。本轮已授权完成文档提交、push 和关联 PR,未授权 merge,也未授权进入 WP1-WP7 的源码实现。 +PR #414 已合并且 Issue #413 已关闭,但 main push CI 的 planning SDD validation 因并发合并后的漂移 baseline 失败。WP0 因此只能标记为 implemented,不能标记为 verified 或 accepted。Issue #416 负责修复固定提交区间验证;在其合并并于 exact main 复验前,不进入 #415 或 WP1-WP7 的源码实现。 ## 18. 决策门禁 diff --git a/scripts/validate_sdd.py b/scripts/validate_sdd.py index 5bc4915c..c8c06d48 100644 --- a/scripts/validate_sdd.py +++ b/scripts/validate_sdd.py @@ -74,6 +74,21 @@ "current_pr", ) +WP0_EVIDENCE_FIELDS = ( + "wp0_pr_base_ref", + "wp0_pr_head_ref", + "wp0_merge_parent_ref", + "wp0_merge_ref", +) + +WP0_IDENTITY_FIELDS = ("wp0_pr_number",) + +WP0_VERIFICATION_FIELDS = ( + "wp0_exact_main_verification_ref", + "wp0_exact_main_verification_run", + "wp0_exact_main_verification_status", +) + ALLOWED_WP_STATUSES = { "proposed", "accepted-design", @@ -154,7 +169,13 @@ def parse_front_matter(sdd: str, errors: list[str]) -> dict[str, str]: errors.append(f"duplicate front matter fields: {duplicates}") fields = dict(pairs) - for field in CURRENT_FIELDS: + required_fields = ( + CURRENT_FIELDS + + WP0_EVIDENCE_FIELDS + + WP0_IDENTITY_FIELDS + + WP0_VERIFICATION_FIELDS + ) + for field in required_fields: if keys.count(field) != 1: errors.append(f"front matter field {field} must occur exactly once") return fields @@ -344,6 +365,45 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) if pr_matches != [fields.get("current_pr")]: errors.append("current WP implementation PR must occur once and match front matter") + wp0_pr_number = fields.get("wp0_pr_number", "") + if not re.fullmatch(r"[1-9][0-9]*", wp0_pr_number): + errors.append("wp0_pr_number must be a positive decimal GitHub PR number") + wp0_pr_matches = re.findall( + r"(?m)^Implementation PR:\[#(?P\d+)\]\(https://github\.com/arana-db/kiwi/pull/(?P=pr)\)。$", + blocks.get("WP0", ""), + ) + if wp0_pr_matches != [wp0_pr_number]: + errors.append("WP0 implementation PR must match immutable wp0_pr_number") + + verification_ref = fields.get("wp0_exact_main_verification_ref", "") + verification_run = fields.get("wp0_exact_main_verification_run", "") + verification_status = fields.get("wp0_exact_main_verification_status", "") + if verification_status not in {"pending", "passed"}: + errors.append( + "wp0_exact_main_verification_status must be pending or passed" + ) + if verification_status == "pending": + if verification_ref != "none" or verification_run != "none": + errors.append( + "pending WP0 exact-main verification must not claim a ref or run" + ) + elif verification_status == "passed": + if not re.fullmatch(r"[0-9a-f]{40}", verification_ref): + errors.append( + "passed WP0 exact-main verification requires a full Git SHA" + ) + if not re.fullmatch(r"[1-9][0-9]*", verification_run): + errors.append( + "passed WP0 exact-main verification requires a GitHub Actions run" + ) + + if statuses.get("WP0") in {"verified", "accepted", "released"}: + if verification_status != "passed": + errors.append( + f"WP0 status {statuses.get('WP0')} requires passed exact-main " + "verification evidence" + ) + table_expectations = { "Current work package": current_work_package, "Status": current_status, @@ -388,6 +448,11 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) baseline_branch = fields.get("baseline_branch", "") if not re.fullmatch(r"[0-9a-f]{40}", baseline_ref): errors.append("baseline_ref must be a full 40-character lowercase Git SHA") + for field in WP0_EVIDENCE_FIELDS: + if not re.fullmatch(r"[0-9a-f]{40}", fields.get(field, "")): + errors.append( + f"{field} must be a full 40-character lowercase Git SHA" + ) baseline_matches = re.findall(r"(?m)^\| Baseline \| ([^|]+) \|$", sdd) if baseline_matches != [f"{baseline_branch}@{baseline_ref}"]: errors.append("current-state table Baseline must match front matter") @@ -433,50 +498,185 @@ def validate_invariants(sdd: str, errors: list[str]) -> int: return len(ids) -def git_changed_paths(root: Path, baseline_ref: str, errors: list[str]) -> set[str]: - object_check = subprocess.run( - ["git", "-C", str(root), "cat-file", "-e", f"{baseline_ref}^{{commit}}"], +def git_changed_paths_between( + root: Path, + base_ref: str, + head_ref: str, + label: str, + errors: list[str], +) -> set[str]: + unavailable: list[str] = [] + for role, ref in (("base", base_ref), ("head", head_ref)): + object_check = subprocess.run( + ["git", "-C", str(root), "cat-file", "-e", f"{ref}^{{commit}}"], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if object_check.returncode != 0: + unavailable.append(f"{role}={ref}") + if unavailable: + errors.append( + f"{label} references are not available as Git commits: {unavailable}" + ) + return set() + + diff = subprocess.run( + ["git", "-C", str(root), "diff", "--name-only", base_ref, head_ref, "--"], capture_output=True, text=True, encoding="utf-8", check=False, ) - if object_check.returncode != 0: - errors.append(f"baseline_ref is not available as a Git commit: {baseline_ref}") + whitespace = subprocess.run( + ["git", "-C", str(root), "diff", "--check", base_ref, head_ref, "--"], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if whitespace.returncode != 0: + details = (whitespace.stdout + whitespace.stderr).strip() + errors.append(f"{label} diff has whitespace errors: {details}") + if diff.returncode != 0: + errors.append(f"unable to compute {label} changed paths") return set() + return { + path.strip().replace("\\", "/") + for path in diff.stdout.splitlines() + if path.strip() + } - diff = subprocess.run( - ["git", "-C", str(root), "diff", "--name-only", baseline_ref, "--"], + +def validate_expected_git_diff( + root: Path, + base_ref: str, + head_ref: str, + label: str, + expected: set[str], + errors: list[str], +) -> None: + changed = git_changed_paths_between(root, base_ref, head_ref, label, errors) + if changed != expected: + errors.append( + f"{label} changed paths differ from the expected artifact registry: " + f"missing={sorted(expected - changed)}, " + f"unexpected={sorted(changed - expected)}" + ) + + +def validate_wp0_git_evidence( + root: Path, + fields: dict[str, str], + expected: set[str], + errors: list[str], +) -> None: + pr_base_ref = fields.get("wp0_pr_base_ref", "") + pr_head_ref = fields.get("wp0_pr_head_ref", "") + pr_base_check = subprocess.run( + ["git", "-C", str(root), "cat-file", "-e", f"{pr_base_ref}^{{commit}}"], capture_output=True, text=True, encoding="utf-8", check=False, ) - whitespace = subprocess.run( - ["git", "-C", str(root), "diff", "--check", baseline_ref, "--"], + if pr_base_check.returncode != 0: + errors.append(f"WP0 PR base ref is not available as a Git commit: {pr_base_ref}") + + pr_head_check = subprocess.run( + ["git", "-C", str(root), "cat-file", "-e", f"{pr_head_ref}^{{commit}}"], capture_output=True, text=True, encoding="utf-8", check=False, ) - untracked = subprocess.run( - ["git", "-C", str(root), "ls-files", "--others", "--exclude-standard"], + if pr_base_check.returncode == 0 and pr_head_check.returncode == 0: + pr_ancestry = subprocess.run( + [ + "git", + "-C", + str(root), + "merge-base", + "--is-ancestor", + pr_base_ref, + pr_head_ref, + ], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if pr_ancestry.returncode != 0: + errors.append("WP0 PR base ref must be an ancestor of the PR head ref") + validate_expected_git_diff( + root, + pr_base_ref, + pr_head_ref, + "WP0 PR base-to-head", + expected, + errors, + ) + + merge_parent_ref = fields.get("wp0_merge_parent_ref", "") + merge_ref = fields.get("wp0_merge_ref", "") + if pr_base_check.returncode == 0: + merge_ancestry = subprocess.run( + [ + "git", + "-C", + str(root), + "merge-base", + "--is-ancestor", + pr_base_ref, + merge_parent_ref, + ], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if merge_ancestry.returncode != 0: + errors.append( + "WP0 PR base ref must be an ancestor of the squash-merge parent" + ) + validate_expected_git_diff( + root, + merge_parent_ref, + merge_ref, + "WP0 merge-parent-to-merge", + expected, + errors, + ) + + parents = subprocess.run( + ["git", "-C", str(root), "rev-list", "--parents", "-n", "1", merge_ref], capture_output=True, text=True, encoding="utf-8", check=False, ) - if whitespace.returncode != 0: - details = (whitespace.stdout + whitespace.stderr).strip() - errors.append(f"WP0 committed/working diff has whitespace errors: {details}") - if diff.returncode != 0 or untracked.returncode != 0: - errors.append("unable to compute WP0 changed paths from baseline_ref") - return set() - return { - path.strip().replace("\\", "/") - for path in (diff.stdout + "\n" + untracked.stdout).splitlines() - if path.strip() - } + expected_lineage = [merge_ref, merge_parent_ref] + actual_lineage = parents.stdout.strip().split() + if parents.returncode != 0 or actual_lineage != expected_lineage: + errors.append( + "WP0 squash-merge evidence must bind the merge commit and its parent exactly: " + f"expected={expected_lineage}, found={actual_lineage}" + ) + + subject = subprocess.run( + ["git", "-C", str(root), "show", "-s", "--format=%s", merge_ref], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + expected_pr_marker = f"(#{fields.get('wp0_pr_number', '')})" + if subject.returncode != 0 or expected_pr_marker not in subject.stdout.strip(): + errors.append( + "WP0 merge commit subject must identify the implementation PR: " + f"expected marker={expected_pr_marker}, found={subject.stdout.strip()!r}" + ) def validate_artifacts( @@ -527,13 +727,8 @@ def validate_artifacts( if len(text.splitlines()) > 20: errors.append(f"legacy pointer must not maintain an independent state copy: {relative}") - if check_git_diff and fields.get("current_work_package") == "WP0": - changed = git_changed_paths(root, fields.get("baseline_ref", ""), errors) - if changed != expected: - errors.append( - "WP0 changed paths differ from the expected artifact registry: " - f"missing={sorted(expected - changed)}, unexpected={sorted(changed - expected)}" - ) + if check_git_diff: + validate_wp0_git_evidence(root, fields, expected, errors) def validate_markdown( @@ -688,6 +883,73 @@ def run_self_tests(root: Path) -> None: if errors: raise AssertionError(f"baseline contract must pass before mutations: {errors}") + field_errors: list[str] = [] + fields = parse_front_matter(read_text(root / ".planning/SDD.md"), field_errors) + if field_errors: + raise AssertionError(f"baseline front matter must parse: {field_errors}") + stale_errors: list[str] = [] + validate_expected_git_diff( + root, + fields["wp0_pr_base_ref"], + fields["wp0_merge_ref"], + "stale-baseline regression", + set(EXPECTED_WP0_ARTIFACTS), + stale_errors, + ) + if not any( + "unexpected=" in error and "src/storage/src/storage.rs" in error + for error in stale_errors + ): + raise AssertionError( + "stale-baseline regression must expose the concurrently merged source paths: " + f"{stale_errors}" + ) + + wrong_parent_fields = dict(fields) + wrong_parent_fields["wp0_merge_parent_ref"] = fields["wp0_pr_base_ref"] + wrong_parent_errors: list[str] = [] + validate_wp0_git_evidence( + root, + wrong_parent_fields, + set(EXPECTED_WP0_ARTIFACTS), + wrong_parent_errors, + ) + if not any("squash-merge evidence" in error for error in wrong_parent_errors): + raise AssertionError( + "wrong merge parent must fail the immutable lineage check: " + f"{wrong_parent_errors}" + ) + + wrong_merge_fields = dict(fields) + wrong_merge_fields["wp0_merge_ref"] = fields["wp0_merge_parent_ref"] + wrong_merge_errors: list[str] = [] + validate_wp0_git_evidence( + root, + wrong_merge_fields, + set(EXPECTED_WP0_ARTIFACTS), + wrong_merge_errors, + ) + if not any("squash-merge evidence" in error for error in wrong_merge_errors): + raise AssertionError( + "wrong merge ref must fail the immutable lineage check: " + f"{wrong_merge_errors}" + ) + + wrong_base_fields = dict(fields) + wrong_base_fields["wp0_pr_base_ref"] = "0" * 40 + wrong_base_errors: list[str] = [] + validate_wp0_git_evidence( + root, + wrong_base_fields, + set(EXPECTED_WP0_ARTIFACTS), + wrong_base_errors, + ) + if not any("PR base ref is not available" in error for error in wrong_base_errors): + raise AssertionError( + "wrong PR base ref must fail even when the PR head object is unavailable: " + f"{wrong_base_errors}" + ) + for relative in ( ".planning/REQUIREMENTS.md", ".planning/DECISIONS.md", @@ -904,6 +1166,39 @@ def break_baseline(candidate: Path) -> None: expect_failure(root, break_baseline, "baseline_ref must be a full") + def break_wp0_evidence_ref(candidate: Path) -> None: + path = candidate / ".planning/SDD.md" + text, replacements = re.subn( + r"(?m)^wp0_pr_head_ref: [0-9a-f]{40}$", + "wp0_pr_head_ref: deadbeef", + read_text(path), + count=1, + ) + if replacements != 1: + raise AssertionError( + "WP0 evidence mutation requires exactly one full wp0_pr_head_ref SHA" + ) + path.write_text(text, encoding="utf-8") + + expect_failure( + root, + break_wp0_evidence_ref, + "wp0_pr_head_ref must be a full", + ) + + def break_wp0_pr_number(candidate: Path) -> None: + path = candidate / ".planning/SDD.md" + path.write_text( + read_text(path).replace("wp0_pr_number: 414", "wp0_pr_number: 999", 1), + encoding="utf-8", + ) + + expect_failure( + root, + break_wp0_pr_number, + "WP0 implementation PR must match immutable wp0_pr_number", + ) + def break_current_anchor(candidate: Path) -> None: path = candidate / ".planning/SDD.md" text = read_text(path).replace('', '', 1) @@ -963,26 +1258,26 @@ def remove_live_gate(candidate: Path) -> None: expect_failure(root, remove_live_gate, "WP0 verification gate contract drifted") - with tempfile.TemporaryDirectory(prefix="kiwi-sdd-lifecycle-") as temporary: - candidate = Path(temporary) - copy_contract(root, candidate) + def promote_wp0_without_exact_main_evidence(candidate: Path) -> None: path = candidate / ".planning/SDD.md" text = read_text(path) - text = text.replace("current_work_package_status: in-progress", "current_work_package_status: implemented", 1) + text = text.replace("current_work_package_status: implemented", "current_work_package_status: verified", 1) wp0 = text.index("### WP0:") prefix, suffix = text[:wp0], text[wp0:] - suffix = suffix.replace("状态:in-progress。", "状态:implemented。", 1) - suffix = suffix.replace("| Status | in-progress |", "| Status | implemented |", 1) + suffix = suffix.replace("状态:implemented。", "状态:verified。", 1) + suffix = suffix.replace("| Status | implemented |", "| Status | verified |", 1) path.write_text(prefix + suffix, encoding="utf-8") - lifecycle_errors, _ = validate( - candidate, check_git_diff=False, check_markdown=False - ) - if lifecycle_errors: - raise AssertionError(f"implemented lifecycle state must remain valid: {lifecycle_errors}") + + expect_failure( + root, + promote_wp0_without_exact_main_evidence, + "WP0 status verified requires passed exact-main verification evidence", + ) print( "SDD validator self-tests passed " - "(25 failure-path mutations, 1 lifecycle transition, 1 prose guard)" + "(28 failure-path mutations, 1 prose guard, " + "4 fixed-ref/concurrent-merge regressions)" ) From 258d031584bc7779b11f376f1740c04478cde63e Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sun, 2 Aug 2026 21:38:13 +0800 Subject: [PATCH 2/4] fix(planning): bind WP0 evidence projections Keep the human-readable WP0 ranges, exact-main status, and current-state table synchronized with the sole machine-readable front matter. Reject duplicate or conflicting projections and stale Issue tracking. Require a passed exact-main ref to exist after the WP0 merge and within the recorded baseline main history, while avoiding misleading ancestry errors when merge objects are unavailable. Add regressions for unrelated verification refs and conflicting evidence lines. Constraint: Preserve offline validation without requiring the squash-merged PR Head object. Confidence: high Scope-risk: narrow Tested: Windows and Ubuntu WSL SDD self-test, validator, and git diff checks. Not-tested: GitHub Actions on this new Head. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .planning/SDD.md | 4 +- scripts/validate_sdd.py | 175 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 171 insertions(+), 8 deletions(-) diff --git a/.planning/SDD.md b/.planning/SDD.md index aa8d3d6e..e4398c30 100644 --- a/.planning/SDD.md +++ b/.planning/SDD.md @@ -197,7 +197,7 @@ Issue 数量和分类会变化,工作包只依赖明确列出的 Issue,不 |---|---|---| | Compaction | [#88](https://github.com/arana-db/kiwi/issues/88) | WP5 物理清理 | | Compaction filter | [#138](https://github.com/arana-db/kiwi/issues/138) | WP5 filter 安装与生命周期 | -| Block cache | [#143](https://github.com/arana-db/kiwi/issues/143) | 支持轨道,等待 PR #412 | +| Block cache | [#143](https://github.com/arana-db/kiwi/issues/143) | 支持轨道,PR #412 已合并,Issue 已关闭,待能力证据评估 | | Error model | [#315](https://github.com/arana-db/kiwi/issues/315) | WP6 | | Test strategy | [#325](https://github.com/arana-db/kiwi/issues/325) | WP1/WP6/WP7 | | Raft apply Epic | [#332](https://github.com/arana-db/kiwi/issues/332) | WP4 Epic | @@ -919,7 +919,7 @@ Post-merge validation repair:[#416](https://github.com/arana-db/kiwi/issues/41 - merge 固定区间:cbcbadc27068634d851ab0ed63989d2214ab2408..9820162ebdf2d26aa6349e704efe8737b2e73e4a; - PR #414 于 2026-08-02T12:17:13Z 合并,Issue #413 随后关闭; - main push CI run 30747510551 / job 91495496924 失败,原因是旧 baseline_ref 把先合并的 PR #412 的 7 个源码路径计入 WP0,而不是 WP0 产物本身失败。 -- WP0 exact-main 成功验证仍为 pending,尚无可写入的成功 ref 或 Actions run。 +- WP0 exact-main verification:status=pending,ref=none,run=none。 Requirement: diff --git a/scripts/validate_sdd.py b/scripts/validate_sdd.py index c8c06d48..e52459fe 100644 --- a/scripts/validate_sdd.py +++ b/scripts/validate_sdd.py @@ -397,6 +397,34 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) "passed WP0 exact-main verification requires a GitHub Actions run" ) + wp0_block = blocks.get("WP0", "") + evidence_projection = ( + ( + "- PR 固定区间:", + f"- PR 固定区间:{fields.get('wp0_pr_base_ref', '')}.." + f"{fields.get('wp0_pr_head_ref', '')};", + ), + ( + "- merge 固定区间:", + f"- merge 固定区间:{fields.get('wp0_merge_parent_ref', '')}.." + f"{fields.get('wp0_merge_ref', '')};", + ), + ( + "- WP0 exact-main verification:", + f"- WP0 exact-main verification:status={verification_status}," + f"ref={verification_ref},run={verification_run}。", + ), + ) + wp0_lines = wp0_block.splitlines() + for prefix, expected_line in evidence_projection: + matching_lines = [line for line in wp0_lines if line.startswith(prefix)] + if matching_lines != [expected_line]: + errors.append( + "WP0 evidence projection must contain exactly one line matching " + f"immutable front matter: expected={expected_line!r}, " + f"found={matching_lines}" + ) + if statuses.get("WP0") in {"verified", "accepted", "released"}: if verification_status != "passed": errors.append( @@ -407,6 +435,7 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) table_expectations = { "Current work package": current_work_package, "Status": current_status, + "WP0 exact-main verification": verification_status, } for label, expected in table_expectations.items(): matches = re.findall(rf"(?m)^\| {re.escape(label)} \| (.*?) \|$", sdd) @@ -620,7 +649,14 @@ def validate_wp0_git_evidence( merge_parent_ref = fields.get("wp0_merge_parent_ref", "") merge_ref = fields.get("wp0_merge_ref", "") - if pr_base_check.returncode == 0: + merge_parent_check = subprocess.run( + ["git", "-C", str(root), "cat-file", "-e", f"{merge_parent_ref}^{{commit}}"], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if pr_base_check.returncode == 0 and merge_parent_check.returncode == 0: merge_ancestry = subprocess.run( [ "git", @@ -664,6 +700,62 @@ def validate_wp0_git_evidence( f"expected={expected_lineage}, found={actual_lineage}" ) + if fields.get("wp0_exact_main_verification_status") == "passed": + verification_ref = fields.get("wp0_exact_main_verification_ref", "") + verification_ref_check = subprocess.run( + ["git", "-C", str(root), "cat-file", "-e", f"{verification_ref}^{{commit}}"], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if verification_ref_check.returncode != 0: + errors.append( + "passed WP0 exact-main verification ref is not available as a Git " + f"commit: {verification_ref}" + ) + else: + verification_ancestry = subprocess.run( + [ + "git", + "-C", + str(root), + "merge-base", + "--is-ancestor", + merge_ref, + verification_ref, + ], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if verification_ancestry.returncode != 0: + errors.append( + "passed WP0 exact-main verification ref must descend from the " + "recorded WP0 merge commit" + ) + baseline_ancestry = subprocess.run( + [ + "git", + "-C", + str(root), + "merge-base", + "--is-ancestor", + verification_ref, + fields.get("baseline_ref", ""), + ], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if baseline_ancestry.returncode != 0: + errors.append( + "passed WP0 exact-main verification ref must belong to the " + "recorded baseline main history" + ) + subject = subprocess.run( ["git", "-C", str(root), "show", "-s", "--format=%s", merge_ref], capture_output=True, @@ -950,6 +1042,30 @@ def run_self_tests(root: Path) -> None: f"{wrong_base_errors}" ) + wrong_verification_fields = dict(fields) + wrong_verification_fields.update( + { + "wp0_exact_main_verification_status": "passed", + "wp0_exact_main_verification_ref": fields["wp0_pr_base_ref"], + "wp0_exact_main_verification_run": "1", + } + ) + wrong_verification_errors: list[str] = [] + validate_wp0_git_evidence( + root, + wrong_verification_fields, + set(EXPECTED_WP0_ARTIFACTS), + wrong_verification_errors, + ) + if not any( + "verification ref must descend" in error + for error in wrong_verification_errors + ): + raise AssertionError( + "unrelated exact-main verification ref must fail ancestry validation: " + f"{wrong_verification_errors}" + ) + for relative in ( ".planning/REQUIREMENTS.md", ".planning/DECISIONS.md", @@ -1188,10 +1304,17 @@ def break_wp0_evidence_ref(candidate: Path) -> None: def break_wp0_pr_number(candidate: Path) -> None: path = candidate / ".planning/SDD.md" - path.write_text( - read_text(path).replace("wp0_pr_number: 414", "wp0_pr_number: 999", 1), - encoding="utf-8", + text, replacements = re.subn( + r"(?m)^wp0_pr_number: [1-9][0-9]*$", + "wp0_pr_number: 999", + read_text(path), + count=1, ) + if replacements != 1: + raise AssertionError( + "WP0 PR mutation requires exactly one wp0_pr_number field" + ) + path.write_text(text, encoding="utf-8") expect_failure( root, @@ -1274,10 +1397,50 @@ def promote_wp0_without_exact_main_evidence(candidate: Path) -> None: "WP0 status verified requires passed exact-main verification evidence", ) + def drift_wp0_evidence_projection(candidate: Path) -> None: + path = candidate / ".planning/SDD.md" + text = read_text(path).replace( + "- WP0 exact-main verification:status=pending,ref=none,run=none。", + "- WP0 exact-main verification:status=passed,ref=none,run=none。", + 1, + ) + path.write_text(text, encoding="utf-8") + + expect_failure( + root, + drift_wp0_evidence_projection, + "WP0 evidence projection must contain exactly one line", + ) + + def duplicate_wp0_evidence_projection(candidate: Path) -> None: + path = candidate / ".planning/SDD.md" + conflict = ( + "- PR 固定区间:" + "1111111111111111111111111111111111111111.." + "2222222222222222222222222222222222222222;" + ) + text, replacements = re.subn( + r"(?m)^- PR 固定区间:.+;$", + lambda match: f"{match.group(0)}\n{conflict}", + read_text(path), + count=1, + ) + if replacements != 1: + raise AssertionError( + "duplicate projection mutation requires exactly one PR range" + ) + path.write_text(text, encoding="utf-8") + + expect_failure( + root, + duplicate_wp0_evidence_projection, + "WP0 evidence projection must contain exactly one line", + ) + print( "SDD validator self-tests passed " - "(28 failure-path mutations, 1 prose guard, " - "4 fixed-ref/concurrent-merge regressions)" + "(30 failure-path mutations, 1 prose guard, " + "5 fixed-ref/concurrent-merge regressions)" ) From 1159d0d1777bb1bafe35e324cd789006ecdea67a Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 3 Aug 2026 16:13:29 +0800 Subject: [PATCH 3/4] fix(planning): harden WP0 verification evidence Pin the immutable PR #414 identity even when its Head object is unavailable, and require passed exact-main evidence to reference a successful ci workflow push on main for the exact recorded SHA. Reject the WP0 merge commit itself as verification evidence. Make lifecycle mutations state-independent and run the immutable-Head and live-run regressions through the public validator entry point so a future wiring regression fails --self-test. Constraint: Keep WP0 implemented and exact-main verification pending; change only SDD governance and validator behavior under Issue #416. Confidence: high Scope-risk: narrow Tested: Windows and Ubuntu WSL external regressions, validator self-tests, main validator, Python compile, working and staged diff checks. Not-tested: Fresh GitHub Actions on the pushed Head. Related: #416 Co-authored-by: OmX Signed-off-by: Xin.Zh --- .planning/SDD.md | 1 + scripts/validate_sdd.py | 381 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 368 insertions(+), 14 deletions(-) diff --git a/.planning/SDD.md b/.planning/SDD.md index e4398c30..61c9fc36 100644 --- a/.planning/SDD.md +++ b/.planning/SDD.md @@ -950,6 +950,7 @@ Requirement: - `git diff --check` 和 committed-diff whitespace check; - `python scripts/validate_sdd.py --self-test` 的失败路径变异测试; - `python scripts/validate_sdd.py` 的 Markdown 链接、占位词、围栏和状态断言; +- WP0 exact-main 状态提升时,在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致; - 63 个 REQ 和 18 个 Decision 的唯一注册、范围展开和引用全集闭包; - WP0、primary Issue #413、PR #414 和 20 个预期产物的一致性断言; - live Issue #413、开放 Issue 数量、关键 PR 状态和远端 main 复核; diff --git a/scripts/validate_sdd.py b/scripts/validate_sdd.py index e52459fe..414960fa 100644 --- a/scripts/validate_sdd.py +++ b/scripts/validate_sdd.py @@ -23,12 +23,15 @@ import argparse from collections import Counter +from collections.abc import Callable import json +import os from pathlib import Path import re import shutil import subprocess import tempfile +import urllib.request REQ_ID = re.compile(r"\bREQ-[A-Z]+-\d{3}\b") @@ -89,6 +92,22 @@ "wp0_exact_main_verification_status", ) +EXPECTED_WP0_IMMUTABLE_EVIDENCE = { + "wp0_pr_number": "414", + "wp0_pr_base_ref": "0c4795ec716299598686fc7c5e0fac03a30e044d", + "wp0_pr_head_ref": "e2bfc7deb481590a757f0034874b7f21a4a31aa2", + "wp0_merge_parent_ref": "cbcbadc27068634d851ab0ed63989d2214ab2408", + "wp0_merge_ref": "9820162ebdf2d26aa6349e704efe8737b2e73e4a", +} + +GITHUB_REPOSITORY = "arana-db/kiwi" +EXPECTED_BASELINE_BRANCH = "main" +EXPECTED_WP0_VERIFICATION_WORKFLOW = { + "name": "ci", + "path": ".github/workflows/ci.yml", +} +GithubRunLoader = Callable[[str], dict[str, object]] + ALLOWED_WP_STATUSES = { "proposed", "accepted-design", @@ -119,6 +138,7 @@ "- `git diff --check` 和 committed-diff whitespace check;", "- `python scripts/validate_sdd.py --self-test` 的失败路径变异测试;", "- `python scripts/validate_sdd.py` 的 Markdown 链接、占位词、围栏和状态断言;", + "- WP0 exact-main 状态提升时,在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致;", f"- {EXPECTED_REQUIREMENT_COUNT} 个 REQ 和 {EXPECTED_DECISION_COUNT} 个 Decision 的唯一注册、范围展开和引用全集闭包;", f"- WP0、primary Issue #413、PR #414 和 {len(EXPECTED_WP0_ARTIFACTS)} 个预期产物的一致性断言;", "- live Issue #413、开放 Issue 数量、关键 PR 状态和远端 main 复核;", @@ -301,6 +321,84 @@ def work_package_blocks(sdd: str) -> dict[str, str]: return blocks +def validate_wp0_immutable_evidence( + fields: dict[str, str], errors: list[str] +) -> None: + for field, expected in EXPECTED_WP0_IMMUTABLE_EVIDENCE.items(): + actual = fields.get(field, "") + if actual != expected: + errors.append(f"{field} must remain {expected}, found {actual}") + + +def fetch_github_actions_run(run_id: str) -> dict[str, object]: + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "kiwi-sdd-validator", + "X-GitHub-Api-Version": "2022-11-28", + } + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request( + f"https://api.github.com/repos/{GITHUB_REPOSITORY}/actions/runs/{run_id}", + headers=headers, + ) + try: + with urllib.request.urlopen(request, timeout=15) as response: + payload = json.loads(response.read().decode("utf-8")) + except (OSError, ValueError) as error: + raise RuntimeError( + f"unable to load GitHub Actions run {run_id}: {type(error).__name__}" + ) from error + if not isinstance(payload, dict): + raise RuntimeError(f"GitHub Actions run {run_id} returned a non-object payload") + return payload + + +def validate_wp0_github_run_evidence( + fields: dict[str, str], run: dict[str, object], errors: list[str] +) -> None: + run_id = fields.get("wp0_exact_main_verification_run", "") + expected: dict[str, object] = { + "id": int(run_id), + "status": "completed", + "conclusion": "success", + "event": "push", + "head_branch": EXPECTED_BASELINE_BRANCH, + "head_sha": fields.get("wp0_exact_main_verification_ref", ""), + **EXPECTED_WP0_VERIFICATION_WORKFLOW, + } + for field, expected_value in expected.items(): + actual = run.get(field) + if actual != expected_value: + errors.append( + "WP0 exact-main GitHub Actions run " + f"{field} must be {expected_value!r}, found {actual!r}" + ) + + +def validate_wp0_exact_main_run( + fields: dict[str, str], + errors: list[str], + loader: GithubRunLoader | None = None, +) -> None: + if fields.get("wp0_exact_main_verification_status") != "passed": + return + run_id = fields.get("wp0_exact_main_verification_run", "") + if not re.fullmatch(r"[1-9][0-9]*", run_id): + return + load_run = loader or fetch_github_actions_run + try: + run = load_run(run_id) + except Exception as error: + errors.append( + f"unable to validate WP0 exact-main GitHub Actions run {run_id}: " + f"{type(error).__name__}: {error}" + ) + return + validate_wp0_github_run_evidence(fields, run, errors) + + def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) -> None: work_package_ids = WP_HEADING.findall(sdd) duplicate_work_packages = duplicate_values(work_package_ids) @@ -368,6 +466,7 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) wp0_pr_number = fields.get("wp0_pr_number", "") if not re.fullmatch(r"[1-9][0-9]*", wp0_pr_number): errors.append("wp0_pr_number must be a positive decimal GitHub PR number") + validate_wp0_immutable_evidence(fields, errors) wp0_pr_matches = re.findall( r"(?m)^Implementation PR:\[#(?P\d+)\]\(https://github\.com/arana-db/kiwi/pull/(?P=pr)\)。$", blocks.get("WP0", ""), @@ -474,7 +573,18 @@ def validate_current_state(sdd: str, fields: dict[str, str], errors: list[str]) errors.append("current-state table PR label and URL must match front matter") baseline_ref = fields.get("baseline_ref", "") + baseline_repository = fields.get("baseline_repository", "") baseline_branch = fields.get("baseline_branch", "") + if baseline_repository != GITHUB_REPOSITORY: + errors.append( + f"baseline_repository must remain {GITHUB_REPOSITORY}, " + f"found {baseline_repository}" + ) + if baseline_branch != EXPECTED_BASELINE_BRANCH: + errors.append( + f"baseline_branch must remain {EXPECTED_BASELINE_BRANCH}, " + f"found {baseline_branch}" + ) if not re.fullmatch(r"[0-9a-f]{40}", baseline_ref): errors.append("baseline_ref must be a full 40-character lowercase Git SHA") for field in WP0_EVIDENCE_FIELDS: @@ -715,6 +825,11 @@ def validate_wp0_git_evidence( f"commit: {verification_ref}" ) else: + if verification_ref == merge_ref: + errors.append( + "passed WP0 exact-main verification ref must strictly descend " + "from the recorded WP0 merge commit" + ) verification_ancestry = subprocess.run( [ "git", @@ -900,6 +1015,8 @@ def validate( check_git_diff: bool = True, check_markdown: bool = True, markdown_paths: tuple[str, ...] | None = None, + check_github_run: bool = True, + github_run_loader: GithubRunLoader | None = None, ) -> tuple[list[str], dict[str, object]]: errors: list[str] = [] sdd_path = root / ".planning/SDD.md" @@ -914,6 +1031,8 @@ def validate( errors.append(f"current_plan must point to {expected_plan}") requirement_count, decision_count = validate_registries(root, sdd, errors) validate_current_state(sdd, fields, errors) + if check_github_run: + validate_wp0_exact_main_run(fields, errors, github_run_loader) validate_wp0_gate_contract(sdd, errors) invariant_count = validate_invariants(sdd, errors) validate_artifacts(root, sdd, fields, errors, check_git_diff) @@ -946,6 +1065,94 @@ def copy_contract(root: Path, destination: Path) -> None: shutil.copy2(source, target) +def replace_once(text: str, pattern: str, replacement: str, label: str) -> str: + updated, replacements = re.subn(pattern, replacement, text, count=1) + if replacements != 1: + raise AssertionError(f"{label} mutation requires exactly one matching line") + return updated + + +def set_wp0_state_text( + sdd: str, + *, + work_package_status: str, + verification_ref: str, + verification_run: str, + verification_status: str, +) -> str: + updated = replace_once( + sdd, + r"(?m)^current_work_package_status: [a-z-]+$", + f"current_work_package_status: {work_package_status}", + "current WP status", + ) + for field, value in ( + ("wp0_exact_main_verification_ref", verification_ref), + ("wp0_exact_main_verification_run", verification_run), + ("wp0_exact_main_verification_status", verification_status), + ): + updated = replace_once( + updated, + rf"(?m)^{re.escape(field)}: .+$", + f"{field}: {value}", + field, + ) + + wp0_start = updated.find("### WP0:") + wp1_start = updated.find("### WP1:", wp0_start) + if wp0_start < 0 or wp1_start < 0: + raise AssertionError("WP0 state mutation requires WP0 and WP1 headings") + prefix = updated[:wp0_start] + wp0 = updated[wp0_start:wp1_start] + suffix = updated[wp1_start:] + wp0 = replace_once( + wp0, + r"(?m)^状态:[a-z-]+。$", + f"状态:{work_package_status}。", + "WP0 block status", + ) + wp0 = replace_once( + wp0, + r"(?m)^- WP0 exact-main verification:.+。$", + "- WP0 exact-main verification:" + f"status={verification_status},ref={verification_ref},run={verification_run}。", + "WP0 evidence projection", + ) + updated = prefix + wp0 + suffix + updated = replace_once( + updated, + r"(?m)^\| Status \| [a-z-]+ \|$", + f"| Status | {work_package_status} |", + "current-state table status", + ) + return replace_once( + updated, + r"(?m)^\| WP0 exact-main verification \| [a-z-]+ \|$", + f"| WP0 exact-main verification | {verification_status} |", + "current-state table exact-main verification", + ) + + +def promote_wp0_without_exact_main_evidence_text(sdd: str) -> str: + return set_wp0_state_text( + sdd, + work_package_status="verified", + verification_ref="none", + verification_run="none", + verification_status="pending", + ) + + +def drift_wp0_evidence_projection_text(sdd: str) -> str: + return replace_once( + sdd, + r"(?m)^- WP0 exact-main verification:.+。$", + "- WP0 exact-main verification:status=invalid,ref=" + f"{EXPECTED_WP0_IMMUTABLE_EVIDENCE['wp0_merge_ref']},run=invalid。", + "WP0 evidence projection drift", + ) + + def expect_failure( root: Path, mutation, @@ -963,6 +1170,7 @@ def expect_failure( check_git_diff=False, check_markdown=check_markdown, markdown_paths=markdown_paths, + check_github_run=False, ) if not any(expected_fragment in error for error in errors): raise AssertionError( @@ -971,7 +1179,7 @@ def expect_failure( def run_self_tests(root: Path) -> None: - errors, _ = validate(root) + errors, _ = validate(root, check_github_run=False) if errors: raise AssertionError(f"baseline contract must pass before mutations: {errors}") @@ -979,6 +1187,7 @@ def run_self_tests(root: Path) -> None: fields = parse_front_matter(read_text(root / ".planning/SDD.md"), field_errors) if field_errors: raise AssertionError(f"baseline front matter must parse: {field_errors}") + stale_errors: list[str] = [] validate_expected_git_diff( root, @@ -1066,6 +1275,155 @@ def run_self_tests(root: Path) -> None: f"{wrong_verification_errors}" ) + equal_verification_fields = dict(fields) + equal_verification_fields.update( + { + "wp0_exact_main_verification_status": "passed", + "wp0_exact_main_verification_ref": fields["wp0_merge_ref"], + "wp0_exact_main_verification_run": "1", + } + ) + equal_verification_errors: list[str] = [] + validate_wp0_git_evidence( + root, + equal_verification_fields, + set(EXPECTED_WP0_ARTIFACTS), + equal_verification_errors, + ) + if not any("strictly descend" in error for error in equal_verification_errors): + raise AssertionError( + "the merge commit itself must not satisfy exact-main verification: " + f"{equal_verification_errors}" + ) + + valid_run: dict[str, object] = { + "id": 30750372362, + "status": "completed", + "conclusion": "success", + "event": "push", + "head_branch": "main", + "head_sha": "2" * 40, + "name": "ci", + "path": ".github/workflows/ci.yml", + } + invalid_run_values: dict[str, object] = { + "id": 30750372363, + "status": "in_progress", + "conclusion": "failure", + "event": "pull_request", + "head_branch": "codex/fix-sdd-post-merge-validation", + "head_sha": "3" * 40, + "name": "Benchmark", + "path": ".github/workflows/benchmark.yml", + } + baseline_sdd = read_text(root / ".planning/SDD.md") + synthetic_passed_sdd = set_wp0_state_text( + baseline_sdd, + work_package_status="verified", + verification_ref="2" * 40, + verification_run="30750372362", + verification_status="passed", + ) + with tempfile.TemporaryDirectory(prefix="kiwi-sdd-integration-test-") as temporary: + candidate = Path(temporary) + copy_contract(root, candidate) + candidate_sdd = candidate / ".planning/SDD.md" + + tampered_head = "1" * 40 + tampered_head_sdd = replace_once( + baseline_sdd, + r"(?m)^wp0_pr_head_ref: [0-9a-f]{40}$", + f"wp0_pr_head_ref: {tampered_head}", + "tampered immutable PR head", + ) + tampered_head_sdd = replace_once( + tampered_head_sdd, + r"(?m)^- PR 固定区间:.+;$", + f"- PR 固定区间:{fields['wp0_pr_base_ref']}..{tampered_head};", + "tampered immutable PR range projection", + ) + candidate_sdd.write_text(tampered_head_sdd, encoding="utf-8") + tampered_head_errors, _ = validate( + candidate, + check_git_diff=False, + check_markdown=False, + check_github_run=False, + ) + if not any( + "wp0_pr_head_ref must remain" in error + for error in tampered_head_errors + ): + raise AssertionError( + "the validate entry point must reject a syntactically valid but " + f"tampered immutable PR head: {tampered_head_errors}" + ) + + candidate_sdd.write_text(synthetic_passed_sdd, encoding="utf-8") + loaded_runs: list[str] = [] + + def load_valid_run(run_id: str) -> dict[str, object]: + loaded_runs.append(run_id) + return dict(valid_run) + + valid_run_errors, _ = validate( + candidate, + check_git_diff=False, + check_markdown=False, + github_run_loader=load_valid_run, + ) + if loaded_runs != ["30750372362"] or valid_run_errors: + raise AssertionError( + "the validate entry point must load and accept matching exact-main " + f"evidence once: loaded={loaded_runs}, errors={valid_run_errors}" + ) + + for field, invalid_value in invalid_run_values.items(): + invalid_run = dict(valid_run) + invalid_run[field] = invalid_value + invalid_run_errors, _ = validate( + candidate, + check_git_diff=False, + check_markdown=False, + github_run_loader=lambda _run_id, run=invalid_run: run, + ) + if not any( + f"run {field} must" in error for error in invalid_run_errors + ): + raise AssertionError( + "the validate entry point must reject GitHub Actions run " + f"{field} mismatch: {invalid_run_errors}" + ) + + evidence_removed_sdd = promote_wp0_without_exact_main_evidence_text( + synthetic_passed_sdd + ) + state_regression_lines = ( + "current_work_package_status: verified", + "wp0_exact_main_verification_ref: none", + "wp0_exact_main_verification_run: none", + "wp0_exact_main_verification_status: pending", + "状态:verified。", + "| Status | verified |", + "| WP0 exact-main verification | pending |", + "- WP0 exact-main verification:status=pending,ref=none,run=none。", + ) + missing_state_lines = [ + line for line in state_regression_lines if line not in evidence_removed_sdd + ] + if evidence_removed_sdd == synthetic_passed_sdd or missing_state_lines: + raise AssertionError( + "verified-without-evidence mutation must work from a passed baseline: " + f"missing={missing_state_lines}" + ) + drifted_passed_sdd = drift_wp0_evidence_projection_text(synthetic_passed_sdd) + if ( + drifted_passed_sdd == synthetic_passed_sdd + or "status=invalid" not in drifted_passed_sdd + ): + raise AssertionError( + "evidence projection drift mutation must work from a passed baseline" + ) + for relative in ( ".planning/REQUIREMENTS.md", ".planning/DECISIONS.md", @@ -1383,13 +1741,10 @@ def remove_live_gate(candidate: Path) -> None: def promote_wp0_without_exact_main_evidence(candidate: Path) -> None: path = candidate / ".planning/SDD.md" - text = read_text(path) - text = text.replace("current_work_package_status: implemented", "current_work_package_status: verified", 1) - wp0 = text.index("### WP0:") - prefix, suffix = text[:wp0], text[wp0:] - suffix = suffix.replace("状态:implemented。", "状态:verified。", 1) - suffix = suffix.replace("| Status | implemented |", "| Status | verified |", 1) - path.write_text(prefix + suffix, encoding="utf-8") + path.write_text( + promote_wp0_without_exact_main_evidence_text(read_text(path)), + encoding="utf-8", + ) expect_failure( root, @@ -1399,12 +1754,10 @@ def promote_wp0_without_exact_main_evidence(candidate: Path) -> None: def drift_wp0_evidence_projection(candidate: Path) -> None: path = candidate / ".planning/SDD.md" - text = read_text(path).replace( - "- WP0 exact-main verification:status=pending,ref=none,run=none。", - "- WP0 exact-main verification:status=passed,ref=none,run=none。", - 1, + path.write_text( + drift_wp0_evidence_projection_text(read_text(path)), + encoding="utf-8", ) - path.write_text(text, encoding="utf-8") expect_failure( root, @@ -1440,7 +1793,7 @@ def duplicate_wp0_evidence_projection(candidate: Path) -> None: print( "SDD validator self-tests passed " "(30 failure-path mutations, 1 prose guard, " - "5 fixed-ref/concurrent-merge regressions)" + "immutable Git/live-run/state regressions)" ) From 72c375c41f819c4b4a5c468226e20bb3a2df606d Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 3 Aug 2026 16:50:37 +0800 Subject: [PATCH 4/4] fix(planning): keep WP0 promotion reachable Document that exact-main promotion must advance baseline_ref with the accepted verification commit. Exercise both the rejected stale-baseline path and the accepted advanced-baseline path against real Git history. Refs: #416 Co-authored-by: OmX Signed-off-by: Xin.Zh --- .planning/SDD.md | 3 ++- scripts/validate_sdd.py | 54 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.planning/SDD.md b/.planning/SDD.md index 61c9fc36..d41c14ab 100644 --- a/.planning/SDD.md +++ b/.planning/SDD.md @@ -920,6 +920,7 @@ Post-merge validation repair:[#416](https://github.com/arana-db/kiwi/issues/41 - PR #414 于 2026-08-02T12:17:13Z 合并,Issue #413 随后关闭; - main push CI run 30747510551 / job 91495496924 失败,原因是旧 baseline_ref 把先合并的 PR #412 的 7 个源码路径计入 WP0,而不是 WP0 产物本身失败。 - WP0 exact-main verification:status=pending,ref=none,run=none。 +- 状态提升为 passed/verified 时,必须同时将 baseline_ref 推进到 verification ref 或其后的 main 提交;保留旧 merge ref 会使验收证据不属于已记录的 baseline main 历史。 Requirement: @@ -950,7 +951,7 @@ Requirement: - `git diff --check` 和 committed-diff whitespace check; - `python scripts/validate_sdd.py --self-test` 的失败路径变异测试; - `python scripts/validate_sdd.py` 的 Markdown 链接、占位词、围栏和状态断言; -- WP0 exact-main 状态提升时,在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致; +- WP0 exact-main 状态提升时,baseline_ref 必须推进到 verification ref 或其后的 main 提交,并在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致; - 63 个 REQ 和 18 个 Decision 的唯一注册、范围展开和引用全集闭包; - WP0、primary Issue #413、PR #414 和 20 个预期产物的一致性断言; - live Issue #413、开放 Issue 数量、关键 PR 状态和远端 main 复核; diff --git a/scripts/validate_sdd.py b/scripts/validate_sdd.py index 414960fa..2a6e49dc 100644 --- a/scripts/validate_sdd.py +++ b/scripts/validate_sdd.py @@ -138,7 +138,7 @@ "- `git diff --check` 和 committed-diff whitespace check;", "- `python scripts/validate_sdd.py --self-test` 的失败路径变异测试;", "- `python scripts/validate_sdd.py` 的 Markdown 链接、占位词、围栏和状态断言;", - "- WP0 exact-main 状态提升时,在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致;", + "- WP0 exact-main 状态提升时,baseline_ref 必须推进到 verification ref 或其后的 main 提交,并在线核验 recorded GitHub Actions run 与 ci workflow、main push、精确 SHA 和 success 结论一致;", f"- {EXPECTED_REQUIREMENT_COUNT} 个 REQ 和 {EXPECTED_DECISION_COUNT} 个 Decision 的唯一注册、范围展开和引用全集闭包;", f"- WP0、primary Issue #413、PR #414 和 {len(EXPECTED_WP0_ARTIFACTS)} 个预期产物的一致性断言;", "- live Issue #413、开放 Issue 数量、关键 PR 状态和远端 main 复核;", @@ -867,8 +867,8 @@ def validate_wp0_git_evidence( ) if baseline_ancestry.returncode != 0: errors.append( - "passed WP0 exact-main verification ref must belong to the " - "recorded baseline main history" + "passed WP0 exact-main verification requires baseline_ref to advance " + "to the verification ref or a later main commit" ) subject = subprocess.run( @@ -1296,6 +1296,52 @@ def run_self_tests(root: Path) -> None: f"{equal_verification_errors}" ) + current_head = subprocess.run( + ["git", "-C", str(root), "rev-parse", "HEAD"], + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ).stdout.strip() + unadvanced_baseline_fields = dict(fields) + unadvanced_baseline_fields.update( + { + "wp0_exact_main_verification_status": "passed", + "wp0_exact_main_verification_ref": current_head, + "wp0_exact_main_verification_run": "1", + } + ) + unadvanced_baseline_errors: list[str] = [] + validate_wp0_git_evidence( + root, + unadvanced_baseline_fields, + set(EXPECTED_WP0_ARTIFACTS), + unadvanced_baseline_errors, + ) + if not any( + "requires baseline_ref to advance" in error + for error in unadvanced_baseline_errors + ): + raise AssertionError( + "passed exact-main promotion must explain the required baseline advance: " + f"{unadvanced_baseline_errors}" + ) + + advanced_baseline_fields = dict(unadvanced_baseline_fields) + advanced_baseline_fields["baseline_ref"] = current_head + advanced_baseline_errors: list[str] = [] + validate_wp0_git_evidence( + root, + advanced_baseline_fields, + set(EXPECTED_WP0_ARTIFACTS), + advanced_baseline_errors, + ) + if advanced_baseline_errors: + raise AssertionError( + "passed exact-main promotion must be reachable after baseline_ref advances: " + f"{advanced_baseline_errors}" + ) + valid_run: dict[str, object] = { "id": 30750372362, "status": "completed", @@ -1793,7 +1839,7 @@ def duplicate_wp0_evidence_projection(candidate: Path) -> None: print( "SDD validator self-tests passed " "(30 failure-path mutations, 1 prose guard, " - "immutable Git/live-run/state regressions)" + "immutable Git/live-run/state regressions, 1 reachable promotion path)" )