feat: govern attachment exceptions and ownership transfers

This commit is contained in:
2026-07-30 17:42:10 +02:00
parent cd223cbb95
commit 5f7503598c
28 changed files with 2229 additions and 180 deletions
@@ -478,7 +478,91 @@ def _recent_failures(jobs: list[CampaignJob], *, limit: int = 20) -> list[dict[s
]
def _job_row(job: CampaignJob, *, include_diagnostics: bool = False) -> dict[str, Any]:
def _review_decisions_by_job(
version: CampaignVersion | None,
) -> dict[str, dict[str, Any]]:
if version is None:
return {}
editor_state = (
getattr(version, "editor_state", None)
if isinstance(getattr(version, "editor_state", None), dict)
else {}
)
review_state = editor_state.get("review_send")
build_summary = (
getattr(version, "build_summary", None)
if isinstance(getattr(version, "build_summary", None), dict)
else {}
)
build_token = str(
build_summary.get("build_token")
or build_summary.get("built_at")
or ""
)
if (
not isinstance(review_state, dict)
or review_state.get("inspection_complete") is not True
or not build_token
or str(review_state.get("build_token") or "") != build_token
):
return {}
decisions: dict[str, dict[str, Any]] = {}
for item in review_state.get("issue_decisions") or []:
if not isinstance(item, dict):
continue
job_id = str(item.get("job_id") or "").strip()
if job_id and job_id not in decisions:
decisions[job_id] = item
return decisions
def _public_review_decision(
decision: dict[str, Any] | None,
*,
include_diagnostics: bool,
) -> dict[str, Any] | None:
if not decision:
return None
result = {
"decision": decision.get("decision"),
"reason": decision.get("reason"),
"actor_user_id": decision.get("actor_user_id"),
"decided_at": decision.get("decided_at"),
"issue_codes": list(decision.get("issue_codes") or []),
}
if include_diagnostics:
result.update(
{
"review_key": decision.get("review_key"),
"message_sha256": decision.get("message_sha256"),
"issue_fingerprint": decision.get("issue_fingerprint"),
}
)
return result
def _review_decision_summary(
decisions: dict[str, dict[str, Any]],
) -> dict[str, Any]:
issue_codes: Counter[str] = Counter()
for decision in decisions.values():
issue_codes.update(
str(code)
for code in decision.get("issue_codes") or []
if code
)
return {
"exception_decision_count": len(decisions),
"by_issue_code": dict(issue_codes),
}
def _job_row(
job: CampaignJob,
*,
include_diagnostics: bool = False,
review_decision: dict[str, Any] | None = None,
) -> dict[str, Any]:
row = {
"job_id": job.id,
"entry_index": job.entry_index,
@@ -519,6 +603,10 @@ def _job_row(job: CampaignJob, *, include_diagnostics: bool = False) -> dict[str
"issues_count": len(job.issues_snapshot or []),
"attachment_config_count": len(job.resolved_attachments or []),
"matched_file_count": sum(len(item.get("matches") or []) for item in (job.resolved_attachments or []) if isinstance(item, dict)),
"review_decision": _public_review_decision(
review_decision,
include_diagnostics=include_diagnostics,
),
}
if include_diagnostics:
row.update(
@@ -592,8 +680,14 @@ def _job_evidence_row(
latest_imap: ImapAppendAttempt | None = None,
latest_message_action: CampaignMessageAction | None = None,
include_diagnostics: bool = False,
review_decision: dict[str, Any] | None = None,
) -> dict[str, Any]:
row = _job_row(job, include_diagnostics=include_diagnostics)
row = _job_row(
job,
include_diagnostics=include_diagnostics,
review_decision=review_decision,
)
public_decision = row.pop("review_decision") or {}
recipients = job.resolved_recipients or {}
row.update({
"campaign_id": job.campaign_id,
@@ -617,7 +711,27 @@ def _job_evidence_row(
if latest_message_action
else None
),
"review_decision": public_decision.get("decision"),
"review_reason": public_decision.get("reason"),
"review_actor_user_id": public_decision.get("actor_user_id"),
"review_decided_at": public_decision.get("decided_at"),
"review_issue_codes": "; ".join(
str(code)
for code in public_decision.get("issue_codes") or []
),
})
if include_diagnostics:
row.update(
{
"review_key": public_decision.get("review_key"),
"review_message_sha256": public_decision.get(
"message_sha256"
),
"review_issue_fingerprint": public_decision.get(
"issue_fingerprint"
),
}
)
return row
@@ -684,6 +798,7 @@ def generate_campaign_report(
)
aggregate = _JobReportAggregate()
job_rows: list[dict[str, Any]] = []
review_decisions = _review_decisions_by_job(version)
retry_max_attempts = _retry_max_attempts(version)
iterator = jobs.yield_per(500) if hasattr(jobs, "yield_per") else jobs
for job in iterator:
@@ -700,7 +815,11 @@ def generate_campaign_report(
"or the paginated recipient table."
)
job_rows.append(
_job_row(job, include_diagnostics=include_diagnostics)
_job_row(
job,
include_diagnostics=include_diagnostics,
review_decision=review_decisions.get(job.id),
)
)
report = _campaign_report_payload(
session,
@@ -710,6 +829,7 @@ def generate_campaign_report(
tenant_id=tenant_id,
include_recent_failures=include_recent_failures,
include_diagnostics=include_diagnostics,
review_decisions=review_decisions,
)
if include_jobs:
report["jobs"] = job_rows
@@ -743,6 +863,7 @@ def _campaign_report_payload(
tenant_id: str,
include_recent_failures: bool,
include_diagnostics: bool,
review_decisions: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
report = {
"generated_at": _utcnow_iso(),
@@ -775,6 +896,7 @@ def _campaign_report_payload(
"by_status": dict(aggregate.attachment_status),
"by_behavior": dict(aggregate.attachment_behavior),
},
"review": _review_decision_summary(review_decisions or {}),
"attempts": _campaign_report_attempt_counts(
session,
tenant_id=tenant_id,
@@ -1199,6 +1321,7 @@ def generate_jobs_csv(
session,
job_ids,
)
review_decisions = _review_decisions_by_job(version)
rows = [
_job_evidence_row(
job,
@@ -1206,6 +1329,7 @@ def generate_jobs_csv(
latest_imap=latest_imap.get(job.id),
latest_message_action=latest_message_actions.get(job.id),
include_diagnostics=include_diagnostics,
review_decision=review_decisions.get(job.id),
)
for job in jobs
]
@@ -1258,10 +1382,22 @@ def generate_jobs_csv(
"latest_message_action_status",
"latest_message_action_id",
"latest_message_action_at",
"review_decision",
"review_reason",
"review_actor_user_id",
"review_decided_at",
"review_issue_codes",
]
if include_diagnostics:
sent_at_index = fieldnames.index("outcome_unknown_at")
fieldnames[sent_at_index:sent_at_index] = ["claimed_at", "smtp_started_at"]
fieldnames.extend(
[
"review_key",
"review_message_sha256",
"review_issue_fingerprint",
]
)
buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
writer.writeheader()