Files
codex-app-server/cloud/audit_wiring.py
T
host 382ec6d62e 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.
2026-11-03 02:49:25 +00:00

747 lines
33 KiB
Python

"""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