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-03 02:49:25 +00:00
1 parent eeb10118d6
commit 382ec6d62e
5 files changed
+1512 -12

No files matched your search

+746
View File
@@ -0,0 +1,746 @@
"""Wiring between three already-landed tracks (no new semantics here):
- track 1 (``cloud/merge_gate_hard.py``, PR #240): the hard merge
gate, five checks + branch protection, machine-readable ``blocks``
codes on its 409 response;
- track 3 (``cloud/audit_pushback.py``, PR #244): the independent
audit + auto-pushback decision, the pushback package, and the QA
re-run rule;
- track 4 (``cloud/ledger.py``, PR #241): the append-only hash-chain
ledger, the only durable store in this wiring.
What this module owns, and nothing else:
1. Translation at the boundary. Track-3 field names and enums are
authoritative for callers; track-4 storage field names are
authoritative on the chain. This module translates, it does not
invent a second ledger schema:
============================ ===================================
track-3 event chain record (track 4 action)
============================ ===================================
audit verdict ``audit_verdict``
pushback round (pushed) ``audit_pushback_round`` (pushed)
fix (landed, re-audit open) ``audit_pushback_round`` with
``fix_diff`` + package marker
``wiring_event="fix"`` (evidence
row only; see the gate reader)
fix + re-audit ``audit_pushback_round``
(re_reviewed, ``fix_diff`` +
``re_review_result``)
rebuild decision / done ``audit_rebuild``
third-failure triage ``audit_third_failure_assessment``
test-change single audit ``audit_test_change``
audit-of-audit correction ``audit_meta_review``
rebuild notice / confirmation ``audit_rebuild_notice``
escalation / blocked quota / ``audit_wiring_event`` (the one
audit sample compat action added to track 4
by this wiring; see below)
============================ ===================================
2. CONDITIONAL mapping, done here and only here. Track 4 stores
verdicts as ``pass`` / ``fail`` only, and track 3 says
CONDITIONAL is not a pass. Every verdict (audit verdict and
re-audit verdict) that arrives as ``"conditional"`` -- or as
anything that is not exactly ``"pass"`` -- is stored as
``"fail"``, with the original value preserved in
``extra.original_verdict`` (verdict rows) or in
``re_review_result.original_verdict`` (pushback rows). The
original conclusion is never overwritten: the chain keeps the
mapped value AND the original, and corrections are separate
``audit_meta_review`` rows, never edits.
3. The gate reader (``pushback_block_for_task``). It replays a
task's chain rows in id order and returns a ``gate_block()``-
shaped dict (``merge_blocked`` / ``release_blocked`` /
``blocked_by="audit_pushback"`` / ``round`` / ``failed_checks``,
plus a ``modules`` detail map). ``cloud/server.py`` ANDs this
with the hard gate's own five checks: either side blocks and
the merge returns 409 with the task status unchanged. The
pushback block applies in every gate mode -- an open audit
pushback is not something a ``record``-mode caller can confirm
away, and ``merge_gate_confirm`` never touches it.
4. The gate-blocks-to-package bridge
(``pushback_package_from_gate``). A hard-gate 409's ``blocks``
codes are machine input to the pushback package: each code maps
to a track-3 gate check (``ci_*`` -> ``ci_green``,
``directory_*`` -> ``directory_isolation``,
``contract_*`` -> ``contract_version``,
``coordinator_*`` -> ``coordinator_approval``,
``review_*`` / ``unresolved_review_comments`` ->
``unresolved_review_comments``). Branch-protection codes have
no track-3 gate-check member, so they travel as the extra check
name ``"branch_protection"`` in the package's failed items --
they are never dropped, and they are never disguised as one of
the ten checks. Callers must take the codes from the 409
``merge_gate`` object, never by parsing audit detail text.
Known translation losses (stated, not hidden):
- Track 3 distinguishes root causes ``"contract"`` vs ``"design"``;
track 4 stores only ``"implementation"`` / ``"contract_design"``.
Both contract and design map to ``"contract_design"`` on rebuild
rows (track 3's rebuild branch treats them identically: refreeze,
no code rebuild). The finer distinction survives on escalation
rows, which carry the original ``root_cause`` verbatim.
- Track 3's third-failure triage verdict ``"early_refreeze"`` has
no track-4 assessment determination (only ``early_rebuild`` /
``continuation_round`` exist there). It is recorded as an
``audit_wiring_event`` of kind ``escalate_refreeze``, never
forced into an assessment row with the wrong determination.
- Track 3's ``"unknown"`` root cause has no track-4 value and is
refused (``ValueError``) rather than guessed: a rebuild row must
say which side the root cause is on.
- A fix-only row is counted by track 4's timeline summary inside
``pushback_rounds`` (that summary counts rows, and a round
already spans two rows there). The gate reader skips fix rows
for state transitions, so the count wart cannot unblock or block
a merge by itself.
What this module does NOT do:
- It does not change any track-1 / track-3 / track-4 semantic:
hard stays fail-closed by default, protection unknown /
unprotected still block in hard mode, confirmation still never
covers a directory violation, the chain stays fail-closed and
append-only, and the cost dual-column rule is untouched.
- It does not create pushback subtasks, send notices, or run
fixes. ``record_*`` only appends chain rows; deriving a fix
subtask from a package is the caller's job (track 3's
``build_pushback_package`` already describes its shape).
- It does not gate ``deploy.py``. There is no server-side release
endpoint in this codebase: the merge endpoint is where
``merge_blocked`` is enforced, and ``release_blocked`` is
returned alongside it so a future release endpoint can enforce
it with this same reader. Deploy remains an operator action
outside this wiring.
"""
from cloud.audit_pushback import GATE_CHECKS
from cloud.audit_pushback import build_pushback_package as _build_package
from cloud.audit_pushback import gate_block as _gate_block
from cloud.ledger import WIRING_EVENT_ACTION
from cloud.ledger import audit_timeline as _audit_timeline
from cloud.merge_gate_hard import block_descriptions as _block_descriptions
from cloud.qa_matrix import (
check_reaudit_reran_links as _check_reran_links,
)
# Hard-gate block code -> track-3 gate check. Branch-protection
# codes have no track-3 member; see BRANCH_PROTECTION_CHECK.
_GATE_BLOCK_TO_CHECK = {}
for _code in (
"ci_evidence_missing", "ci_not_green", "ci_check_failed",
"ci_required_checks_unconfigured", "ci_required_check_missing"):
_GATE_BLOCK_TO_CHECK[_code] = "ci_green"
for _code in (
"directory_evidence_missing", "directory_owned_dir_unknown",
"directory_isolation_violation"):
_GATE_BLOCK_TO_CHECK[_code] = "directory_isolation"
for _code in (
"contract_version_missing", "contract_not_frozen",
"contract_version_mismatch"):
_GATE_BLOCK_TO_CHECK[_code] = "contract_version"
for _code in (
"coordinator_approval_missing", "coordinator_not_approved"):
_GATE_BLOCK_TO_CHECK[_code] = "coordinator_approval"
for _code in ("review_evidence_missing", "unresolved_review_comments"):
_GATE_BLOCK_TO_CHECK[_code] = "unresolved_review_comments"
# Branch-protection codes (hard-mode codes from track 1's #240
# layer plus record-mode codes from the #209 layer).
for _code in ("branch_protection_unknown",
"branch_protection_unprotected",
"branch_protection_enforce",
"branch_protection_risk_unconfirmed"):
_GATE_BLOCK_TO_CHECK[_code] = "branch_protection"
BRANCH_PROTECTION_CHECK = "branch_protection"
# Track-3 decision actions that block merge/release (mirrors
# track 3's gate_block() membership; kept here so the package
# builder does not have to re-derive it).
BLOCKING_ACTIONS = (
"pushback", "rebuild", "escalate", "escalate_refreeze",
"blocked_quota", "rebuild_pending_user_confirmation",
)
# Wiring-event kinds that block (compat action rows).
_BLOCKING_WIRING_KINDS = ("escalate", "escalate_refreeze",
"blocked_quota")
# Triage verdict -> track-4 assessment determination. "early_refreeze"
# is deliberately absent: it goes to record_escalation instead.
_TRIAGE_TO_DETERMINATION = {
"early_rebuild": "early_rebuild",
"targeted_fix": "continuation_round",
}
# Track-3 root cause -> track-4 root cause. "unknown" is absent
# on purpose (refused, not guessed).
_ROOT_CAUSE_MAP = {
"implementation": "implementation",
"contract": "contract_design",
"design": "contract_design",
}
def normalize_verdict(verdict):
"""Track-3 verdict -> (track-4 verdict, original).
Exactly ``"pass"`` stays a pass; anything else -- including
``"conditional"`` and missing/garbage values -- maps to
``"fail"`` (fail-closed), with the original preserved for the
chain. The caller stores the original alongside; this function
never drops it silently.
"""
if verdict == "pass":
return "pass", "pass"
original = verdict if isinstance(verdict, str) else None
return "fail", original
def normalize_re_review_result(result):
"""Copy of a re-review result with its verdict normalized.
``CONDITIONAL`` re-audits do not pass (track 3); the stored
verdict becomes ``"fail"`` and the original is kept under
``original_verdict``. A result without a verdict dict is
returned unchanged (the ledger validates the shape).
"""
if not isinstance(result, dict):
return result
copied = dict(result)
normalized, original = normalize_verdict(result.get("verdict"))
copied["verdict"] = normalized
if normalized == "fail" and original != "fail":
copied["original_verdict"] = original
return copied
def gate_failed_checks(gate):
"""Hard-gate 409 -> (track-3 checks, extra checks), in order.
Track-3 checks come first in GATE_CHECKS order, then the
branch-protection extra (if any). Unknown codes pass through
as extras rather than being dropped: a code the wiring does
not recognise must still show up in the package.
"""
checks = []
extras = []
for code in (gate or {}).get("blocks") or []:
mapped = _GATE_BLOCK_TO_CHECK.get(code)
if mapped == BRANCH_PROTECTION_CHECK:
if BRANCH_PROTECTION_CHECK not in extras:
extras.append(BRANCH_PROTECTION_CHECK)
elif mapped is not None:
if mapped not in checks:
checks.append(mapped)
elif code not in extras:
extras.append(code)
order = {name: index for index, name in enumerate(GATE_CHECKS)}
checks.sort(key=lambda name: order.get(name, len(order)))
return checks, extras
def gate_block_items(gate):
"""One pushback-package failed item per hard-gate block code.
The package builder also appends decision ``failed_checks``
that are not already items, so the item ``check`` values here
use the mapped track-3 names (branch protection keeps its
extra name) to avoid duplicate generic entries.
"""
descriptions = _block_descriptions((gate or {}).get("blocks") or [])
items = []
for code, description in zip((gate or {}).get("blocks") or [],
descriptions):
check = _GATE_BLOCK_TO_CHECK.get(code, code)
items.append({
"check": check,
"detail": description,
"evidence": {
"test_name": None,
"file_path": None,
"line_no": None,
"contract_clause": None,
"diff_range": None,
},
"reproduction_steps": [
"re-run the merge gate with full evidence; "
"the failing code is %s" % code,
],
"reaudit_criterion": "re-run the full re-audit checklist "
"for this check",
})
return items
def pushback_package_from_gate(gate, task_id, round_no,
decision_action="pushback",
previous_artifacts=None,
qa_failed_links=None):
"""Build a track-3 pushback package from a hard-gate 409.
``gate`` is the ``merge_gate`` object from the 409 response
(not audit text). ``round_no`` is the track-3 decision round
(0 for the first audit); the ledger round number is derived
by the caller (see ``record_pushback``'s docstring). The
package carries the QA failed links verbatim when given, so
the re-audit must re-run exactly those links (see
``record_reaudit``'s QA guard).
"""
if decision_action not in BLOCKING_ACTIONS and \
decision_action != "allow":
raise ValueError("unknown decision action: %r" % (decision_action,))
checks, extras = gate_failed_checks(gate)
audit = {
"task_id": task_id,
"verdict": "fail",
"verdict_text": "merge gate blocked: %s" % (
", ".join((gate or {}).get("blocks") or ["unknown"])),
"failed_items": gate_block_items(gate),
}
decision = {
"round": round_no,
"next_round": round_no + 1,
"action": decision_action,
"failed_checks": checks + extras,
}
return _build_package(audit, decision, previous_artifacts,
qa_failed_links=qa_failed_links)
def _require_module_contract(module_id, contract_version):
"""Wiring strictness: pushback rows must be attributable.
Track 4 allows module/contract to be None on pushback rows,
but unattributed rows cannot be scoped by the gate reader, so
this wiring refuses them instead of writing rows the gate
cannot reason about. Verdict/rebuild rows already require
both in track 4 itself.
"""
if not isinstance(module_id, str) or not module_id:
raise ValueError("module_id is required for wiring records")
if not isinstance(contract_version, str) or not contract_version:
raise ValueError("contract_version is required for wiring records")
def record_audit_verdict(store, task_id, project_id, module_id,
contract_version, verdict, source,
failures=None, user_id="system", extra=None):
"""Append one audit/acceptance verdict to the chain.
``verdict`` uses track-3 vocabulary (``pass`` / ``fail`` /
``conditional``); ``"conditional"`` is stored as ``"fail"``
with the original in ``extra.original_verdict``.
"""
normalized, original = normalize_verdict(verdict)
merged_extra = dict(extra or {})
if normalized == "fail" and original != "fail":
merged_extra.setdefault("original_verdict", original)
return store.append_audit_verdict(
task_id, project_id, module_id, contract_version,
normalized, source, failures=failures, user_id=user_id,
extra=merged_extra)
def record_pushback(store, task_id, project_id, module_id,
contract_version, round_no, trigger_items,
package, failure_source=None, failure_counts=None,
user_id="system"):
"""Append one pushed pushback round to the chain.
``round_no`` is the LEDGER round number, starting at 1 for
the first pushback. Track 3's ``decide_pushback`` counts
rounds already used (0 for the first audit) and returns
``next_round``; for a pushback action that value IS the
ledger round number (first failure: decision round 0,
next_round 1, ledger round_no 1). Trigger items are the
package's failed checks/items (machine codes, not prose).
"""
_require_module_contract(module_id, contract_version)
return store.append_pushback_round(
task_id, project_id, round_no, trigger_items, package,
user_id=user_id, module_id=module_id,
contract_version=contract_version,
failure_source=failure_source, failure_counts=failure_counts)
def record_fix(store, task_id, project_id, module_id,
contract_version, round_no, fix_diff, package,
user_id="system"):
"""Append fix evidence for a round whose re-audit is still open.
The row carries ``fix_diff`` (summary + sha, never the full
diff) and a package copy marked ``wiring_event="fix"``. The
gate reader skips such rows for state transitions: landing a
fix does not unblock anything -- only a passing re-audit does.
When fix and re-audit arrive together, use ``record_reaudit``
with ``fix_diff`` instead of writing both rows.
"""
_require_module_contract(module_id, contract_version)
marked = dict(package or {})
marked["wiring_event"] = "fix"
return store.append_pushback_round(
task_id, project_id, round_no, _package_trigger_items(package),
marked, fix_diff=fix_diff, user_id=user_id,
module_id=module_id, contract_version=contract_version)
def _package_trigger_items(package):
"""Trigger items for a fix row: the package's failed checks."""
if isinstance(package, dict):
checks = package.get("failed_checks")
if isinstance(checks, list) and checks:
return list(checks)
return ["audit_fix"]
def check_reaudit_qa_guard(package, re_review_result, rerun_results):
"""Fail-closed QA guard for a claimed passing re-audit.
If the package lists ``reaudit_must_rerun_links`` and the
re-audit claims a pass, every listed link must have a passing
re-run result; a missing or failed re-run raises ValueError
instead of letting a false pass reach the chain. A failing
re-audit needs no guard (it stays blocked either way).
Returns the normalized re-review result.
"""
normalized_result = normalize_re_review_result(re_review_result)
if normalized_result.get("verdict") != "pass":
return normalized_result
required = (package or {}).get("reaudit_must_rerun_links") or []
if not required:
return normalized_result
checked = _check_reran_links(package, rerun_results or {})
if not checked["reran_all_failed_links"]:
raise ValueError(
"re-audit claims pass but QA failed links were not "
"all re-run passing: missing=%s failed=%s"
% (checked["missing"], checked["failed"]))
return normalized_result
def record_reaudit(store, task_id, project_id, module_id,
contract_version, round_no, re_review_result,
package=None, fix_diff=None, rerun_results=None,
failure_source=None, failure_counts=None,
user_id="system"):
"""Append one re-audited pushback round to the chain.
This is the row that can unblock: a stored ``pass`` verdict
here clears the module's block in the gate reader, a stored
``fail`` (including a mapped ``conditional``) keeps it
blocked. The QA guard in ``check_reaudit_qa_guard`` runs
before anything is written, so a claimed pass with un-rerun
or failed QA links never reaches the chain.
"""
_require_module_contract(module_id, contract_version)
guarded = check_reaudit_qa_guard(package, re_review_result,
rerun_results)
return store.append_pushback_round(
task_id, project_id, round_no,
_package_trigger_items(package), package or {},
fix_diff=fix_diff, re_review_result=guarded,
user_id=user_id, module_id=module_id,
contract_version=contract_version,
failure_source=failure_source, failure_counts=failure_counts)
def record_rebuild(store, task_id, project_id, module_id,
contract_version, rebuild_no, old_impl_ref,
new_impl_ref=None, root_cause="implementation",
status="decided", prior_failure_total=None,
retained_assets=None, trigger_source=None,
assessment_entry_id=None, user_id="system"):
"""Append one rebuild decision / completion to the chain.
``root_cause`` uses track-3 vocabulary; ``"contract"`` and
``"design"`` both store as track-4 ``"contract_design"``
(see the module docstring). ``"unknown"`` is refused.
A ``rebuilt`` row does NOT unblock the gate on its own: the
new implementation still needs a passing re-audit.
"""
mapped = _ROOT_CAUSE_MAP.get(root_cause)
if mapped is None:
raise ValueError(
"root_cause must be implementation|contract|design, "
"not %r (unknown is refused, not guessed)" % (root_cause,))
return store.append_rebuild_decision(
task_id, project_id, module_id, contract_version,
rebuild_no, old_impl_ref, new_impl_ref=new_impl_ref,
root_cause=mapped, status=status,
prior_failure_total=prior_failure_total,
user_id=user_id, retained_assets=retained_assets,
trigger_source=trigger_source,
assessment_entry_id=assessment_entry_id)
def record_third_failure_assessment(store, task_id, project_id,
module_id, contract_version,
triage_verdict, basis,
failure_total=3,
max_extra_rounds=None,
user_id="system"):
"""Append one third-failure triage determination to the chain.
Track-3 triage verdicts map as ``early_rebuild`` ->
``early_rebuild`` and ``targeted_fix`` -> ``continuation_round``.
``early_refreeze`` has no assessment determination in track 4
and is refused here: record it with ``record_escalation``
(kind ``escalate_refreeze``) instead.
"""
determination = _TRIAGE_TO_DETERMINATION.get(triage_verdict)
if determination is None:
raise ValueError(
"triage_verdict %r has no assessment determination; "
"use record_escalation(kind='escalate_refreeze') for "
"early_refreeze" % (triage_verdict,))
return store.append_third_failure_assessment(
task_id, project_id, module_id, contract_version,
determination, basis, failure_total=failure_total,
max_extra_rounds=max_extra_rounds, user_id=user_id)
def record_test_change(store, task_id, project_id, module_id,
contract_version, test_changes, auditor,
contract_refrozen, refreeze_ref=None,
reason="", user_id="system"):
"""Append one test-change single audit (track-4 native)."""
return store.append_test_change_audit(
task_id, project_id, module_id, contract_version,
test_changes, auditor, contract_refrozen,
refreeze_ref=refreeze_ref, reason=reason, user_id=user_id)
def record_meta_review(store, task_id, project_id, module_id,
contract_version, original_entry_id,
original_verdict, corrected_verdict,
misjudgment_found, reviewer, findings=None,
triggered_process=None, user_id="system"):
"""Append one audit-of-audit correction (history is kept).
The correction never rewrites the original row. How the gate
reader treats it is a resolved semantic tension, stated here
rather than buried (see ``pushback_block_for_task``): a
correction to ``fail`` (a wrong pass) re-blocks immediately,
but a correction to ``pass`` (a wrong block) does NOT unblock
by itself -- track 3's restored action for that case is
"schedule a re-audit", not "release the gate", and a
meta-review is a review of an audit, not a full re-audit.
Unblocking still needs a fresh passing verdict or a passing
re-reviewed pushback row. Track 4's failure counts are
unaffected by this choice: counts use effective (corrected)
verdicts to decide rebuild triggers, while the gate asks a
different question ("has a passing re-audit been recorded
since the block?"), so the two may legitimately disagree
while a scheduled re-audit is still pending.
"""
return store.append_audit_of_audit(
task_id, project_id, module_id, contract_version,
original_entry_id, original_verdict, corrected_verdict,
misjudgment_found, reviewer, findings=findings,
triggered_process=triggered_process, user_id=user_id)
def record_rebuild_notice(store, task_id, project_id, module_id,
contract_version, rebuild_no,
notification_content, notified_party,
confirmation_status, confirmed_by=None,
user_id="system"):
"""Append one rebuild-notice row (pending -> confirmed are two rows)."""
return store.append_rebuild_notice(
task_id, project_id, module_id, contract_version,
rebuild_no, notification_content, notified_party,
confirmation_status, confirmed_by=confirmed_by,
user_id=user_id)
def record_escalation(store, task_id, project_id, module_id,
contract_version, kind, target=None, reason="",
round_no=None, root_cause=None,
failed_checks=None, user_id="system"):
"""Append one escalation / quota / audit-sample wiring event.
``kind`` is track-3 vocabulary: ``escalate``,
``escalate_refreeze``, ``blocked_quota``, ``audit_sample``.
Escalation kinds block the gate; ``audit_sample`` (a sampling
selection, not a verdict) does not change gate state. The
original ``root_cause`` (including the contract/design
distinction) is stored verbatim here.
"""
_require_module_contract(module_id, contract_version)
return store.append_wiring_event(
task_id, project_id, module_id, contract_version, kind,
target=target, reason=reason, round_no=round_no,
root_cause=root_cause, failed_checks=failed_checks,
user_id=user_id)
def _trigger_check_names(trigger_items):
"""Trigger items (str or dict) -> check-name strings."""
names = []
for item in trigger_items or []:
if isinstance(item, str):
names.append(item)
elif isinstance(item, dict) and item.get("check"):
names.append(item["check"])
else:
names.append(str(item))
return names
def _module_key(parsed):
"""(module_id, contract_version) or None for unattributed rows."""
module_id = parsed.get("module_id") if isinstance(parsed, dict) else None
contract_version = parsed.get("contract_version") \
if isinstance(parsed, dict) else None
if module_id and contract_version:
return (module_id, contract_version)
return None
def pushback_block_for_task(conn, task_id):
"""Replay a task's chain into a gate_block()-shaped verdict.
State machine, in chain-id order, per (module, contract
version) plus one global slot for unattributed rows:
- verdict row: its stored verdict sets blocked (pass clears,
fail blocks).
- meta-review row: a correction to ``fail`` blocks at once
(a wrong pass is re-blocked); a correction to ``pass``
changes nothing by itself -- the wrong block stays blocked
until a fresh passing verdict or re-reviewed row arrives
(track 3's "schedule a re-audit", not "release the gate").
This is deliberately NOT track 4's effective-verdict
counting: counts and gate state answer different
questions, and conflating them would let a review of an
audit substitute for a re-audit.
- pushback pushed: blocked. pushback re_reviewed: blocked
unless the stored re-audit verdict is a pass (a stored
``conditional`` was already mapped to fail at write time;
anything non-pass here still blocks, fail-closed).
- pushback fix-evidence row (package ``wiring_event="fix"``):
no state change.
- rebuild row (decided or rebuilt): blocked -- a rebuilt
implementation still needs a passing re-audit.
- wiring event escalate / escalate_refreeze / blocked_quota:
blocked. audit_sample: no change.
- rebuild notice pending: blocked. confirmed / rejected: no
change (confirmation alone never unblocks).
- assessment / test-change / meta rows: no direct change
(meta rows act only through corrections).
A task is blocked if the global slot or any module slot is
blocked. A missing ``audit_ledger`` table means no chain has
ever been written for this database, which is unblocked, not
an error; any other read failure propagates so the server can
fail closed (see server.py).
"""
try:
timeline = _audit_timeline(conn, task_id=task_id, limit=10000)
except Exception as exc:
if "no such table" in str(exc):
return _gate_block("allow", [], 0)
raise
entries = timeline.get("entries") or []
modules = {}
global_state = {"blocked": False, "round": None, "checks": []}
last_blocked_round = None
def slot_for(parsed):
key = _module_key(parsed)
if key is None:
return None, global_state
if key not in modules:
modules[key] = {"blocked": False, "round": None,
"checks": []}
return key, modules[key]
for entry in entries:
parsed = entry.get("parsed")
if not isinstance(parsed, dict):
continue
action = entry.get("action")
_key, slot = slot_for(parsed)
if action == "audit_verdict":
blocked = parsed.get("verdict") != "pass"
slot["blocked"] = blocked
slot["checks"] = _trigger_check_names(
parsed.get("failures")) if blocked else []
elif action == "audit_meta_review":
# Re-block on a corrected fail; a corrected pass
# schedules a re-audit and does not unblock (see the
# module docstring: correction != re-audit).
if parsed.get("misjudgment_found") and \
parsed.get("corrected_verdict") == "fail":
slot["blocked"] = True
if not slot["checks"]:
slot["checks"] = ["audit_meta_review"]
elif action == "audit_pushback_round":
package = parsed.get("pushback_package") or {}
if package.get("wiring_event") == "fix":
continue
round_no = parsed.get("round_no")
if parsed.get("round_status") == "pushed":
slot["blocked"] = True
slot["round"] = round_no
slot["checks"] = _trigger_check_names(
parsed.get("trigger_items"))
last_blocked_round = round_no
elif parsed.get("round_status") == "re_reviewed":
result = parsed.get("re_review_result") or {}
blocked = result.get("verdict") != "pass"
slot["blocked"] = blocked
slot["round"] = round_no
slot["checks"] = _trigger_check_names(
parsed.get("trigger_items")) if blocked else []
if blocked:
last_blocked_round = round_no
elif action == "audit_rebuild":
slot["blocked"] = True
if not slot["checks"]:
slot["checks"] = ["audit_rebuild"]
last_blocked_round = slot["round"]
elif action == WIRING_EVENT_ACTION:
if parsed.get("wiring_kind") in _BLOCKING_WIRING_KINDS:
slot["blocked"] = True
checks = parsed.get("failed_checks")
if checks:
slot["checks"] = list(checks)
if parsed.get("round_no") is not None:
slot["round"] = parsed.get("round_no")
last_blocked_round = parsed.get("round_no")
elif action == "audit_rebuild_notice":
if parsed.get("confirmation_status") == "pending":
slot["blocked"] = True
blocked = global_state["blocked"] or any(
slot["blocked"] for slot in modules.values())
checks = []
if global_state["blocked"]:
checks.extend(global_state["checks"])
for slot in modules.values():
if slot["blocked"]:
for check in slot["checks"]:
if check not in checks:
checks.append(check)
block = _gate_block(
"pushback" if blocked else "allow", checks,
last_blocked_round if last_blocked_round is not None else 0)
block["modules"] = {
"%s@%s" % key: {
"blocked": slot["blocked"],
"round": slot["round"],
"failed_checks": list(slot["checks"]),
}
for key, slot in modules.items()
}
if global_state["blocked"]:
block["modules"]["<global>"] = {
"blocked": True,
"round": global_state["round"],
"failed_checks": list(global_state["checks"]),
}
return block
+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
+53
View File
@@ -89,6 +89,7 @@ from cloud.peer_messages import row_get as _row_get
from cloud.marker_tools import parse_spawn_defaults as _parse_spawn_defaults
from cloud.merge_gate import evaluate_merge_gate as _merge_gate_evaluate
from cloud.merge_gate import protection_from_env as _merge_gate_protection
from cloud.audit_wiring import pushback_block_for_task as _pushback_block
from cloud.merge_gate_hard import blocked_audit_detail as _gate_blocked_detail
from cloud.merge_gate_hard import evaluate_hard_merge_gate as _merge_gate_hard
from cloud.merge_gate_hard import hard_mode_from_env as _merge_gate_hard_mode
@@ -8087,6 +8088,18 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
门禁阻断(任一模式)都写 merge_gate_blocked 审计后才回 409,
这是第 3 轨自动推回的阻塞依据:推回包取响应 merge_gate 的
blocks 码,不要解析审计文本;本接口不建推回任务、不改状态机。
审计推回接线(cloud/audit_wiring,第 1 项接线缺口):五项
检查之外再与链上推回阻塞做“与”:链上任一模块仍阻塞
(merge_blocked)时,即使五项全过也 409、状态不变,并在
merge_gate.blocks 里追加 audit_pushback_blocked 码、响应
里附 audit_pushback 对象(merge_blocked / release_blocked
/ blocked_by="audit_pushback" / round / failed_checks)。
推回阻塞与门禁模式无关:record 模式的显式确认只覆盖
保护风险,绝不覆盖推回阻塞;链读不到(audit_ledger 表
不存在)视为无推回状态,其它读链失败一律 fail-closed
409,不放行。注意:本接口没有独立的发布端点,
release_blocked 随响应返回、由未来的发布端点用同一
reader 强制;deploy.py 是运维脚本,不在本接口门禁内。
本服务没有可信渠道直连 GitHub 核实 CI/保护:门禁强制的是
“调用方提交的证据齐全且全绿”,谎报证据由审计与 PR/Checks
只读核对覆盖,不在本接口的验证范围内。
@@ -8147,6 +8160,46 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
approved_exceptions=body.get("approved_exceptions") or (),
protection=protection, mode=mode,
explicit_confirm=bool(body.get("merge_gate_confirm")))
# 审计推回接线:链上阻塞与门禁五项为“与”关系,且与模式
# 无关。读链失败(表不存在除外,见 audit_wiring)时
# fail-closed:不证明无阻塞就不放行。
pushback = None
try:
conn = self._db()
try:
pushback = _pushback_block(conn, task_id)
finally:
conn.close()
except Exception:
pushback = None
if pushback is None:
try:
with _auth_store(self.server.db_path) as store:
store.audit(
user_id, task["project_id"],
"merge_gate_blocked",
"%s mode=%s blocks=audit_pushback_state_unavailable"
% (task_id, gate.get("mode", "hard")))
except Exception:
pass
self._send_json(409, {
"error": "audit pushback state unavailable",
"merge_gate": gate,
"audit_pushback": {
"merge_blocked": True,
"release_blocked": True,
"blocked_by": "audit_pushback_state_unavailable",
"round": None,
"failed_checks": [],
},
})
return
gate["audit_pushback"] = pushback
if pushback["merge_blocked"]:
gate["allowed"] = False
blocks = gate.setdefault("blocks", [])
if "audit_pushback_blocked" not in blocks:
blocks.append("audit_pushback_blocked")
if not gate["allowed"]:
with _auth_store(self.server.db_path) as store:
store.audit(user_id, task["project_id"],
+17 -11
View File
@@ -696,9 +696,13 @@ tests/ # QA 线程专属,含跨端一致性用例
`release_blocked`、`blocked_by="audit_pushback"`、轮次、
失败检查清单),硬门禁自己的检查与推回阻塞是“与”关系:
审计通过不解硬门禁的锁,硬门禁通过也不解推回的锁,两边都
通过才能合并/发布。代码侧该接口目前是提案(第 1 轨落地时
确认接线,在此之前本模块本身不拦任何服务端点,这一点要
如实写进 PR 摘要,不冒充已接线)。
通过才能合并/发布。接线已落地(`cloud/audit_wiring.py`):
服务端 POST /tasks/{id}/merge 在五项检查之外再读链上推回
阻塞,任一阻塞即 409、状态不变;推回阻塞与门禁模式无关,
显式确认绝不覆盖它;每轮推回/修复/重审/推倒/升级都写进
第 4 轨哈希链账本(字段以本轨为准,第 4 轨加了
`audit_wiring_event` 兼容动作,见该模块)。CONDITIONAL
在接线处映射成 fail 再记,原结论保留在链上不抹历史。
- 推回包必须可执行,缺一项就不算推回完成:审计结论原文;
失败项逐条(含测试名、文件路径、行号、合同条款、diff 范围);
复现步骤;修复验收标准(即重新审计时要过的同一批检查,
@@ -723,10 +727,12 @@ tests/ # QA 线程专属,含跨端一致性用例
不因“是修复”就绕过配额。
- 状态与账本:每轮推回、修复、重审都要写审计记录(轮次、触发项、
推回包、修复 diff、重审结果),账本走第 4 轨的防篡改机制;
本轨先按“审计记录字段 + 校验接口”定义(代码里
`build_ledger_entry` / `validate_ledger_chain` 的
`prev_hash` / `entry_hash` 链式格式是提案,第 4 轨落地后接线,
接口不一致处在 PR 摘要写清)。任务时间线要能看到
接线已落地(`cloud/audit_wiring.py`):推回/修复/重审/
推倒/升级按该模块的映射表写进第 4 轨哈希链,链式校验走
第 4 轨的 `verify_chain` / 导出头哈希;本轨的
`build_ledger_entry` / `validate_ledger_chain` 仍是纯函数
提案格式(供离线校验),链上权威格式以第 4 轨为准。
任务时间线要能看到
“审计 fail → 自动推回 → 修复 → 重审”的完整链路,不能只有
最终通过一条记录把失败轮次抹掉。
- 与已有回路的关系,不重复造轮子:G5 缺陷回路(缺陷回 G4、
@@ -837,10 +843,10 @@ tests/ # QA 线程专属,含跨端一致性用例
- 模块失败计数(审核/验收分来源、合计)、推回轮次、推倒次数、
重做新旧实现对应关系、根因判定结果都要写审计记录,走第 4 轨
的防篡改机制(本轨先按字段 + 校验接口定义:账本条目含
`module_id`、`contract_version`、`fail_counts`、`rebuild_no`、
`root_cause`、`old_implementation_ref`,链式校验见代码
`build_ledger_entry` / `validate_ledger_chain`,第 4 轨落地后接线)。
的防篡改机制(接线已落地,见 `cloud/audit_wiring.py` 的
映射表:账本条目含 `module_id`、`contract_version`、
`fail_counts`、`rebuild_no`、`root_cause`、
`old_implementation_ref`,链式校验走第 4 轨)。
- 任务时间线要能看到完整链路:“审计 fail → 自动推回修复
(1–3 轮)→ 超过 3 次 → 推倒重做 → 重新审计”,不能只有最终
通过一条记录;推倒记录、重新冻结升级记录同样留痕,不得抹掉。
+627
View File
@@ -0,0 +1,627 @@
"""Audit-gap-1 wiring: track 3 (pushback) <-> track 1 (hard gate)
<-> track 4 (hash-chain ledger), through cloud/audit_wiring.py.
What is pinned here (and nowhere else):
- CONDITIONAL maps to fail at the wiring boundary, original kept.
- Hard-gate 409 blocks codes become pushback-package failed
checks (branch protection travels as its own extra, never
dropped, never disguised as one of the ten gate checks).
- Every pushback / fix / re-audit / rebuild / escalation step
lands on the hash chain, and the chain still verifies.
- The server ANDs the chain's pushback block with the hard
gate's five checks: audit fail -> merge 409, status
unchanged, chain intact -> fix -> re-audit pass -> merge 200.
This chain runs against the real HTTP endpoint, not just
pure functions.
- The QA full chain is covered separately: a package carrying
QA failed links cannot be re-audited to a pass until exactly
those links have been re-run passing.
- The resolved meta-review tension: a correction to fail
re-blocks at once, but a correction to pass does NOT unblock
by itself (track 3: schedule a re-audit, not release the
gate); only a fresh passing verdict / re-audit unblocks.
Track 4's failure counts still use effective verdicts, so
the two views may disagree while a re-audit is pending --
that disagreement is asserted, not papered over.
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.audit_pushback import GATE_CHECKS
from cloud.audit_pushback import decide_pushback
from cloud.audit_wiring import gate_failed_checks
from cloud.audit_wiring import normalize_re_review_result
from cloud.audit_wiring import normalize_verdict
from cloud.audit_wiring import pushback_block_for_task
from cloud.audit_wiring import pushback_package_from_gate
from cloud.audit_wiring import record_audit_verdict
from cloud.audit_wiring import record_escalation
from cloud.audit_wiring import record_fix
from cloud.audit_wiring import record_meta_review
from cloud.audit_wiring import record_pushback
from cloud.audit_wiring import record_reaudit
from cloud.audit_wiring import record_rebuild
from cloud.audit_wiring import record_third_failure_assessment
from cloud.auth import AuthStore
from cloud.ledger import LedgerStore
from cloud.ledger import audit_timeline
from cloud.merge_gate_hard import evaluate_hard_merge_gate
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.audit_pushback import GATE_CHECKS
from cloud.audit_pushback import decide_pushback
from cloud.audit_wiring import gate_failed_checks
from cloud.audit_wiring import normalize_re_review_result
from cloud.audit_wiring import normalize_verdict
from cloud.audit_wiring import pushback_block_for_task
from cloud.audit_wiring import pushback_package_from_gate
from cloud.audit_wiring import record_audit_verdict
from cloud.audit_wiring import record_escalation
from cloud.audit_wiring import record_fix
from cloud.audit_wiring import record_meta_review
from cloud.audit_wiring import record_pushback
from cloud.audit_wiring import record_reaudit
from cloud.audit_wiring import record_rebuild
from cloud.audit_wiring import record_third_failure_assessment
from cloud.auth import AuthStore
from cloud.ledger import LedgerStore
from cloud.ledger import audit_timeline
from cloud.merge_gate_hard import evaluate_hard_merge_gate
from cloud.server import create_server
MODULE = "apps/web"
CONTRACT = "v2"
def _failing_gate():
return evaluate_hard_merge_gate(
files_changed=["apps/web/a.py"], owned_dir="apps/web",
protection="protected", ci_status="pending",
ci_checks=[{"name": "ci", "status": "pending"}],
required_checks=["ci"], contract_version="v2",
contract_frozen=True, coordinator_approved=True,
unresolved_review_comments=0)
def _full_gate():
return evaluate_hard_merge_gate(
files_changed=["apps/web/a.py"], owned_dir="apps/web",
protection="protected", ci_status="success",
ci_checks=[{"name": "ci", "status": "success"}],
required_checks=["ci"], contract_version="v2",
contract_frozen=True, coordinator_approved=True,
unresolved_review_comments=0)
def _failing_audit():
checks = {name: True for name in GATE_CHECKS}
checks["ci_green"] = False
return {
"task_id": "task-1",
"verdict": "fail",
"verdict_text": "audit fail: ci not green",
"signer": "auditor-a",
"executor": "executor-e",
"coordinator_approver": "coordinator-c",
"qa_tester": "qa-q",
"security_verdict": "pass",
"checks": checks,
"failed_items": [],
"deviations": [],
"regression": {"items": [
{"path": "apps/web/app.py", "sha256_manifest": "a" * 64,
"sha256_recomputed": "a" * 64, "source": "exact"},
]},
"demo": {"verified_runnable": True, "runs_app_entry": True,
"command": "python3 apps/web/app.py"},
}
class VerdictNormalizationTestCase(unittest.TestCase):
def test_conditional_maps_to_fail_and_keeps_original(self):
normalized, original = normalize_verdict("conditional")
self.assertEqual(normalized, "fail")
self.assertEqual(original, "conditional")
normalized, original = normalize_verdict("pass")
self.assertEqual((normalized, original), ("pass", "pass"))
normalized, original = normalize_verdict(None)
self.assertEqual(normalized, "fail")
self.assertIsNone(original)
def test_re_review_conditional_maps_to_fail(self):
result = normalize_re_review_result({"verdict": "conditional"})
self.assertEqual(result["verdict"], "fail")
self.assertEqual(result["original_verdict"], "conditional")
result = normalize_re_review_result({"verdict": "pass"})
self.assertEqual(result["verdict"], "pass")
self.assertNotIn("original_verdict", result)
class GatePackageTestCase(unittest.TestCase):
def test_all_block_families_map_and_branch_protection_survives(self):
gate = evaluate_hard_merge_gate(
files_changed=["apps/other/x.py"], owned_dir="apps/web",
protection="unknown", ci_status="pending",
ci_checks=[{"name": "ci", "status": "failure"}],
required_checks=["ci"], contract_version=None,
contract_frozen=False, coordinator_approved=None,
unresolved_review_comments=["c1"])
self.assertFalse(gate["allowed"])
checks, extras = gate_failed_checks(gate)
for name in ("ci_green", "directory_isolation",
"contract_version", "coordinator_approval",
"unresolved_review_comments"):
self.assertIn(name, checks)
self.assertIn("branch_protection", extras)
package = pushback_package_from_gate(gate, "task-1", 0)
self.assertIn("branch_protection", package["failed_checks"])
self.assertIn("ci_green", package["failed_checks"])
# Full re-audit criteria, not failed-items-only.
self.assertEqual(package["reaudit_criteria"],
list(GATE_CHECKS))
# No block code is lost in translation.
self.assertTrue(package["failed_items"])
def test_qa_links_carried_onto_package(self):
links = [{"link": "checkout-flow", "case": "pay",
"failure": "500", "evidence": "log",
"reproduction": ["run checkout"]}]
package = pushback_package_from_gate(
_failing_gate(), "task-1", 0, qa_failed_links=links)
self.assertEqual(package["qa_failed_links"], links)
self.assertEqual(package["reaudit_must_rerun_links"], links)
class LedgerRecordingTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.store = LedgerStore(self.db_path)
self.task = "task-1"
self.project = "proj-1"
def tearDown(self):
try:
self.store.close()
except Exception:
pass
self.tmp.cleanup()
def _block(self):
conn = sqlite3.connect(self.db_path)
try:
return pushback_block_for_task(conn, self.task)
finally:
conn.close()
def _chain_ok(self):
result = self.store.verify()
self.assertTrue(result["ok"], result)
return result
def test_conditional_verdict_stored_as_fail_with_original(self):
entry = record_audit_verdict(
self.store, self.task, self.project, MODULE, CONTRACT,
"conditional", "audit", failures=["ci_green"])
timeline = audit_timeline(self.store.conn, task_id=self.task)
verdict = [e for e in timeline["entries"]
if e["action"] == "audit_verdict"][0]
self.assertEqual(verdict["parsed"]["verdict"], "fail")
self.assertEqual(verdict["parsed"]["extra"]["original_verdict"],
"conditional")
self.assertEqual(entry["id"], verdict["id"])
self._chain_ok()
def test_pushback_fix_reaudit_cycle_moves_gate(self):
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "fail", "audit",
failures=["ci_green"])
package = pushback_package_from_gate(_failing_gate(),
self.task, 0)
record_pushback(self.store, self.task, self.project, MODULE,
CONTRACT, 1, package["failed_checks"], package,
failure_source="audit")
block = self._block()
self.assertTrue(block["merge_blocked"])
self.assertTrue(block["release_blocked"])
self.assertEqual(block["blocked_by"], "audit_pushback")
self.assertEqual(block["round"], 1)
self.assertIn("ci_green", block["failed_checks"])
# A landed fix alone does not unblock.
record_fix(self.store, self.task, self.project, MODULE,
CONTRACT, 1,
{"summary": "fix ci", "diff_sha256": "b" * 64,
"files_changed": ["apps/web/a.py"]}, package)
self.assertTrue(self._block()["merge_blocked"])
# A passing re-audit unblocks; chain still verifies.
record_reaudit(self.store, self.task, self.project, MODULE,
CONTRACT, 1, {"verdict": "pass"},
package=package)
self.assertFalse(self._block()["merge_blocked"])
self._chain_ok()
def test_qa_guard_refuses_unrerun_or_failed_link_pass(self):
links = [{"link": "checkout-flow", "case": "pay",
"failure": "500", "evidence": "log",
"reproduction": ["run checkout"]}]
package = pushback_package_from_gate(
_failing_gate(), self.task, 0, qa_failed_links=links)
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "fail", "audit",
failures=["ci_green"])
record_pushback(self.store, self.task, self.project, MODULE,
CONTRACT, 1, package["failed_checks"], package,
failure_source="audit")
before = self.store.verify()["count"]
with self.assertRaises(ValueError):
record_reaudit(self.store, self.task, self.project,
MODULE, CONTRACT, 1, {"verdict": "pass"},
package=package, rerun_results={})
with self.assertRaises(ValueError):
record_reaudit(self.store, self.task, self.project,
MODULE, CONTRACT, 1, {"verdict": "pass"},
package=package,
rerun_results={"checkout-flow": False})
self.assertEqual(self.store.verify()["count"], before)
self.assertTrue(self._block()["merge_blocked"])
record_reaudit(self.store, self.task, self.project, MODULE,
CONTRACT, 1, {"verdict": "pass"},
package=package,
rerun_results={"checkout-flow": True})
self.assertFalse(self._block()["merge_blocked"])
def test_rebuild_alone_does_not_unblock(self):
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "fail", "audit",
failures=["ci_green"])
record_rebuild(self.store, self.task, self.project, MODULE,
CONTRACT, 1, {"commit": "old123"},
root_cause="implementation", status="decided")
self.assertTrue(self._block()["merge_blocked"])
record_rebuild(self.store, self.task, self.project, MODULE,
CONTRACT, 1, {"commit": "old123"},
new_impl_ref={"commit": "new456"},
root_cause="implementation", status="rebuilt",
retained_assets={"old_tests_status": "pass"})
self.assertTrue(self._block()["merge_blocked"])
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "pass", "audit")
self.assertFalse(self._block()["merge_blocked"])
self._chain_ok()
def test_escalation_blocks_sample_does_not(self):
record_escalation(self.store, self.task, self.project,
MODULE, CONTRACT, "blocked_quota",
target="coordinator",
reason="fix exceeds quota", round_no=2,
failed_checks=["ci_green"])
self.assertTrue(self._block()["merge_blocked"])
record_escalation(self.store, self.task, self.project,
MODULE, CONTRACT, "audit_sample",
reason="every 10th record")
self.assertTrue(self._block()["merge_blocked"])
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "pass", "audit")
self.assertFalse(self._block()["merge_blocked"])
def test_meta_review_tension_is_pinned(self):
fail_entry = record_audit_verdict(
self.store, self.task, self.project, MODULE, CONTRACT,
"fail", "audit", failures=["ci_green"])
self.assertTrue(self._block()["merge_blocked"])
# Wrong block corrected to pass: gate STAYS blocked until
# a fresh passing re-audit (track 3 restored action).
record_meta_review(self.store, self.task, self.project,
MODULE, CONTRACT, fail_entry["id"],
"fail", "pass", True, "reviewer-r",
findings=["wrong call"],
triggered_process="re-review")
self.assertTrue(self._block()["merge_blocked"])
# ...while track 4 counts use the effective verdict.
counts = self.store.module_failure_counts(MODULE, CONTRACT,
task_id=self.task)
self.assertEqual(counts["total_fails"], 0)
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "pass", "audit")
self.assertFalse(self._block()["merge_blocked"])
# Wrong pass corrected to fail: re-blocks at once.
pass_entry = record_audit_verdict(
self.store, self.task, self.project, MODULE, CONTRACT,
"pass", "audit")
self.assertFalse(self._block()["merge_blocked"])
record_meta_review(self.store, self.task, self.project,
MODULE, CONTRACT, pass_entry["id"],
"pass", "fail", True, "reviewer-r",
findings=["missed ci"],
triggered_process="re-review")
self.assertTrue(self._block()["merge_blocked"])
self._chain_ok()
def test_modules_are_scoped_not_global(self):
record_audit_verdict(self.store, self.task, self.project,
"apps/a", CONTRACT, "fail", "audit",
failures=["ci_green"])
record_audit_verdict(self.store, self.task, self.project,
"apps/b", CONTRACT, "pass", "audit")
self.assertTrue(self._block()["merge_blocked"])
record_audit_verdict(self.store, self.task, self.project,
"apps/a", CONTRACT, "pass", "audit")
self.assertFalse(self._block()["merge_blocked"])
def test_triage_and_root_cause_mapping(self):
record_third_failure_assessment(
self.store, self.task, self.project, MODULE, CONTRACT,
"targeted_fix",
{"audit_conclusion": "nearly done",
"failure_items": ["ci_green"],
"passed_items": ["directory_isolation"]})
timeline = audit_timeline(self.store.conn, task_id=self.task)
assessment = [e for e in timeline["entries"]
if e["action"] == "audit_third_failure_assessment"][0]
self.assertEqual(assessment["parsed"]["determination"],
"continuation_round")
with self.assertRaises(ValueError):
record_third_failure_assessment(
self.store, self.task, self.project, MODULE, CONTRACT,
"early_refreeze", {"audit_conclusion": "contract"})
with self.assertRaises(ValueError):
record_rebuild(self.store, self.task, self.project,
MODULE, CONTRACT, 1, {"commit": "old"},
root_cause="unknown")
# Design root cause stores as contract_design.
record_rebuild(self.store, self.task, self.project, MODULE,
CONTRACT, 1, {"commit": "old"},
root_cause="design", status="decided")
timeline = audit_timeline(self.store.conn, task_id=self.task)
rebuild = [e for e in timeline["entries"]
if e["action"] == "audit_rebuild"][0]
self.assertEqual(rebuild["parsed"]["root_cause"],
"contract_design")
self._chain_ok()
def test_missing_ledger_table_is_unblocked_not_error(self):
fresh = os.path.join(self.tmp.name, "empty.db")
conn = sqlite3.connect(fresh)
try:
block = pushback_block_for_task(conn, "nope")
finally:
conn.close()
self.assertFalse(block["merge_blocked"])
self.assertIsNone(block["blocked_by"])
def test_decide_pushback_round_feeds_ledger_round(self):
decision = decide_pushback(_failing_audit(), 0)
self.assertEqual(decision["action"], "pushback")
self.assertEqual(decision["next_round"], 1)
record_audit_verdict(self.store, self.task, self.project,
MODULE, CONTRACT, "fail", "audit",
failures=decision["failed_checks"])
package = pushback_package_from_gate(
_failing_gate(), self.task, decision["round"])
record_pushback(self.store, self.task, self.project, MODULE,
CONTRACT, decision["next_round"],
package["failed_checks"], package,
failure_source="audit")
block = self._block()
self.assertTrue(block["merge_blocked"])
self.assertEqual(block["round"], 1)
class EndpointChainTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
self._saved_env = {k: os.environ.get(k) for k in
("CLOUD_MERGE_GATE_MODE",
"CLOUD_BRANCH_PROTECTION")}
os.environ.pop("CLOUD_MERGE_GATE_MODE", None)
os.environ.pop("CLOUD_BRANCH_PROTECTION", None)
self.ledger = LedgerStore(self.db_path)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
for key, value in self._saved_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
try:
self.ledger.close()
except Exception:
pass
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
hdrs = {}
payload = None
if body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs["Content-Type"] = "application/json"
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
def _completed_task(self):
status, task = self._req(
"POST", "/tasks",
{"project_id": self.project["id"], "prompt": "x"},
self.owner_key)
self.assertEqual(status, 201)
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET status = 'completed' WHERE id = ?",
(task["id"],))
conn.commit()
finally:
conn.close()
return task
def _hard_body(self, **overrides):
body = {
"files_changed": ["apps/web/a.py"],
"owned_dir": "apps/web",
"branch_protection": "protected",
"ci_status": "success",
"ci_checks": [{"name": "ci", "status": "success"}],
"required_checks": ["ci"],
"contract_version": "v2",
"contract_frozen": True,
"coordinator_approved": True,
"unresolved_review_comments": 0,
}
body.update(overrides)
return body
def _status_of(self, task_id):
_s, got = self._req("GET", "/tasks/%s" % task_id,
key=self.owner_key)
return got["status"]
def _block_task(self, task, package=None):
if package is None:
package = pushback_package_from_gate(_failing_gate(),
task["id"], 0)
record_audit_verdict(self.ledger, task["id"],
self.project["id"], MODULE, CONTRACT,
"fail", "audit",
failures=package["failed_checks"])
record_pushback(self.ledger, task["id"], self.project["id"],
MODULE, CONTRACT, 1, package["failed_checks"],
package, failure_source="audit")
return package
def test_audit_fail_blocks_merge_fix_reaudit_releases(self):
task = self._completed_task()
package = self._block_task(task)
status, blocked = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 409, blocked)
self.assertIn("audit_pushback_blocked",
blocked["merge_gate"]["blocks"])
self.assertTrue(
blocked["merge_gate"]["audit_pushback"]["merge_blocked"])
self.assertTrue(
blocked["merge_gate"]["audit_pushback"]["release_blocked"])
self.assertEqual(
blocked["merge_gate"]["audit_pushback"]["blocked_by"],
"audit_pushback")
self.assertEqual(self._status_of(task["id"]), "completed")
# Fix + passing re-audit (no QA links on this package).
record_fix(self.ledger, task["id"], self.project["id"],
MODULE, CONTRACT, 1,
{"summary": "fix ci", "diff_sha256": "b" * 64,
"files_changed": ["apps/web/a.py"]}, package)
status, still = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 409)
record_reaudit(self.ledger, task["id"], self.project["id"],
MODULE, CONTRACT, 1, {"verdict": "pass"},
package=package)
status, merged = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 200, merged)
self.assertEqual(self._status_of(task["id"]), "merged")
self.assertFalse(
merged["merge_gate"]["audit_pushback"]["merge_blocked"])
chain = self.ledger.verify()
self.assertTrue(chain["ok"], chain)
def test_qa_failed_links_gate_the_release(self):
task = self._completed_task()
links = [{"link": "checkout-flow", "case": "pay",
"failure": "500", "evidence": "log",
"reproduction": ["run checkout"]}]
package = pushback_package_from_gate(
_failing_gate(), task["id"], 0, qa_failed_links=links)
self._block_task(task, package=package)
status, _blocked = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 409)
# Claimed pass without a passing re-run of the failed
# link must not reach the chain, and must not unblock.
with self.assertRaises(ValueError):
record_reaudit(self.ledger, task["id"],
self.project["id"], MODULE, CONTRACT, 1,
{"verdict": "pass"}, package=package,
rerun_results={"checkout-flow": False})
status, _still = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 409)
record_reaudit(self.ledger, task["id"], self.project["id"],
MODULE, CONTRACT, 1, {"verdict": "pass"},
package=package,
rerun_results={"checkout-flow": True})
status, merged = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 200, merged)
def test_pushback_blocks_even_in_record_mode(self):
task = self._completed_task()
self._block_task(task)
os.environ["CLOUD_MERGE_GATE_MODE"] = "record"
status, blocked = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(), self.owner_key)
self.assertEqual(status, 409, blocked)
self.assertIn("audit_pushback_blocked",
blocked["merge_gate"]["blocks"])
self.assertEqual(self._status_of(task["id"]), "completed")
def test_hard_gate_failure_without_pushback_has_no_pushback_code(self):
task = self._completed_task()
status, blocked = self._req(
"POST", "/tasks/%s/merge" % task["id"],
self._hard_body(ci_status="pending"), self.owner_key)
self.assertEqual(status, 409, blocked)
self.assertIn("ci_not_green", blocked["merge_gate"]["blocks"])
self.assertNotIn("audit_pushback_blocked",
blocked["merge_gate"]["blocks"])
self.assertFalse(
blocked["merge_gate"]["audit_pushback"]["merge_blocked"])
self.assertEqual(self._status_of(task["id"]), "completed")