audit: wire pushback gate to hard merge gate and hash-chain ledger (gap 1)

Track 3 decisions (audit_pushback) now really block the server merge
endpoint and really land on the track 4 hash chain, via the new
cloud/audit_wiring.py translation layer:

- POST /tasks/{id}/merge ANDs the chain pushback block
  (merge_blocked/release_blocked/blocked_by=audit_pushback/round/
  failed_checks, replayed per module+contract version) with the
  hard gate's five checks in every gate mode; either side blocks
  -> 409, status unchanged, audit_pushback_blocked appended to
  merge_gate.blocks. Ledger reads that fail (other than a missing
  audit_ledger table) fail closed to 409.
- Every pushback/fix/re-audit/rebuild/escalation step is written
  to the ledger with track 3 fields authoritative and track 4
  storage authoritative; CONDITIONAL maps to fail at the boundary
  with the original preserved, history never rewritten.
- Hard-gate 409 blocks codes feed pushback packages machine-first
  (branch protection survives as its own extra check).
- Re-audit QA guard: a claimed pass with un-rerun or failed QA
  links never reaches the chain.
- Track 4 compat: one audit_wiring_event action for escalation /
  blocked-quota / audit-sample kinds, counted in the timeline.
- Resolved tension (documented in audit_wiring.py): a meta-review
  correction to fail re-blocks at once, but a correction to pass
  does NOT unblock by itself -- track 3 schedules a re-audit,
  it does not release the gate; track 4 failure counts still use
  effective verdicts, so the two views may disagree while a
  re-audit is pending.

Tests: cloud/tests/test_audit_wiring.py (18 items), incl. a real
HTTP end-to-end chain (fail -> 409 -> fix -> re-audit pass ->
200) and a separate QA-link chain test.
This commit is contained in:
host committed 2026-11-04 20:55:49 +00:00
1 parent 04a7a4280b
commit 4c38e3c452
5 files changed
+1512 -12

No files matched your search

+69 -1
View File
@@ -153,9 +153,21 @@ ASSESSMENT_ACTION = "audit_third_failure_assessment"
TEST_CHANGE_ACTION = "audit_test_change"
META_REVIEW_ACTION = "audit_meta_review"
REBUILD_NOTICE_ACTION = "audit_rebuild_notice"
# Wiring compat action (audit-gap-1 wiring, PR #244 -> #241): track 3
# event kinds with no native track-4 action -- escalation outcomes,
# blocked-quota outcomes, and audit-sample selections. These are
# annotations on the timeline, not verdicts: they never feed
# module_failure_counts, and only the blocking kinds move the
# wiring gate (see cloud/audit_wiring.py). The action name is
# intentionally generic so no second ledger vocabulary grows here:
# one compat action, with the track-3 kind inside the detail.
WIRING_EVENT_ACTION = "audit_wiring_event"
WIRING_KINDS = ("escalate", "escalate_refreeze", "blocked_quota",
"audit_sample")
TIMELINE_ACTIONS = (VERDICT_ACTION, PUSHBACK_ACTION, REBUILD_ACTION,
ASSESSMENT_ACTION, TEST_CHANGE_ACTION,
META_REVIEW_ACTION, REBUILD_NOTICE_ACTION)
META_REVIEW_ACTION, REBUILD_NOTICE_ACTION,
WIRING_EVENT_ACTION)
_VERDICT_VALUES = ("pass", "fail")
_FAILURE_SOURCES = ("audit", "acceptance")
@@ -1049,6 +1061,8 @@ def audit_timeline(conn, task_id=None, module_id=None,
1 for e in entries if e["action"] == META_REVIEW_ACTION),
"rebuild_notices": sum(
1 for e in entries if e["action"] == REBUILD_NOTICE_ACTION),
"wiring_events": sum(
1 for e in entries if e["action"] == WIRING_EVENT_ACTION),
"counts_by_contract": counts_by_contract,
"anomalies": anomalies,
}
@@ -1291,6 +1305,55 @@ def append_rebuild_notice(conn, task_id, project_id, module_id,
task_id=task_id, created_at=created_at)
def append_wiring_event(conn, task_id, project_id, module_id,
contract_version, wiring_kind, target=None,
reason="", round_no=None, root_cause=None,
failed_checks=None, user_id="system",
created_at=None):
"""记一次接线兼容事件(审计加固第 1 项接线缺口新增)。
- wiring_kind: escalate | escalate_refreeze | blocked_quota |
audit_sample(第 3 轨词汇,原文进链;其余第 3 轨事件
各有原生动作,不走这里,见 cloud/audit_wiring.py)。
- 本动作不参与 module_failure_counts(不是结论),时间线
只把它计入 wiring_events;门禁是否阻塞由接线层按 kind
判定(escalate / escalate_refreeze / blocked_quota 阻塞,
audit_sample 不动状态)。
- root_cause 原文保存(contract 与 design 的区分只在
这里保留,audit_rebuild 行只存 contract_design)。
校验失败抛 ValueError,绝不写半条记录。
"""
_require_non_empty_str(task_id, "task_id")
_require_non_empty_str(module_id, "module_id")
_require_non_empty_str(contract_version, "contract_version")
if wiring_kind not in WIRING_KINDS:
raise ValueError(
"wiring_kind must be one of %s" % (WIRING_KINDS,))
if round_no is not None:
_require_int_ge(round_no, "round_no", 0)
if failed_checks is not None and not isinstance(failed_checks, list):
raise ValueError("failed_checks must be a list or None")
if target is not None:
_require_non_empty_str(target, "target")
if root_cause is not None:
_require_non_empty_str(root_cause, "root_cause")
if not isinstance(reason, str):
raise ValueError("reason must be a string")
detail = json.dumps({
"module_id": module_id,
"contract_version": contract_version,
"wiring_kind": wiring_kind,
"target": target,
"reason": reason,
"round_no": round_no,
"root_cause": root_cause,
"failed_checks": failed_checks or [],
}, sort_keys=True, ensure_ascii=False)
return append_entry(conn, user_id, project_id,
WIRING_EVENT_ACTION, detail=detail,
task_id=task_id, created_at=created_at)
class LedgerStore:
"""db_path 版账本存储(测试与脚本用;服务端走连接版函数)。
@@ -1391,3 +1454,8 @@ class LedgerStore:
entry = append_rebuild_notice(self.conn, *args, **kwargs)
self.conn.commit()
return entry
def append_wiring_event(self, *args, **kwargs):
entry = append_wiring_event(self.conn, *args, **kwargs)
self.conn.commit()
return entry