"""Shared per-fingerprint operator-status comment substrate. Houses the dedup substrate every dispatcher uses to post operator- visibility comments on PR / issue timelines without re-implementing the fingerprinting logic in two places. The reviewer dispatcher (:mod:`_review_post`) and the implementer dispatcher (``tools/dispatch_implementer.py`` via :mod:`_review_post`) both key off this module; their public posters (``post_operator_status_comment``, ``post_implementer_status_comment``) are thin per-pipeline wrappers that supply the marker family + body template. Public surface: - ``_sanitize_status_detail`` — normalise + cap free-form reason strings before they enter the fingerprint or the body. - ``_compute_status_fingerprint`` — sha256-derived 12-hex-char identifier for ``(action, sanitised(reason))``. - ``_has_existing_operator_status`` — generic "marker prefix + fingerprint already on this PR" predicate (parameterised on the pipeline's marker prefix). - ``OPERATOR_STATUS_MARKER_PREFIX`` / ``IMPLEMENTER_STATUS_MARKER_PREFIX`` (and their suffix counterparts) — the on-wire marker namespaces. Two-key namespacing protects each pipeline's idempotency from cross-talk if a future audit enumerates "every status comment one dispatcher kind posted on a given PR". - ``operator_status_marker`` / ``implementer_status_marker`` — public marker constructors so external callers don't have to string-concat the prefix + fingerprint themselves. The module is import-safe and self-contained; no cycle risk against :mod:`_review_post` or any dispatcher driver. Extracting it here prevents :mod:`_review_post` from drifting into a junk drawer of "reviewer-specific posters AND a shared dedup substrate AND the implementer's marker family" — the previous shape paid the cohesion price for backwards compatibility. """ from __future__ import annotations import hashlib import re from typing import Any # ─── Reviewer-side marker family ──────────────────────────────────────────── # # Format: ```` # # Stable wire string: lands in PR comment bodies; mutating it would # break fingerprint compatibility with previously-posted comments. _OPERATOR_STATUS_MARKER_PREFIX = "" OPERATOR_STATUS_MARKER_PREFIX = _OPERATOR_STATUS_MARKER_PREFIX OPERATOR_STATUS_MARKER_SUFFIX = _OPERATOR_STATUS_MARKER_SUFFIX def _operator_status_marker(fingerprint: str) -> str: """Build the full HTML-comment marker for ``fingerprint``.""" return ( f"{_OPERATOR_STATUS_MARKER_PREFIX}" f"{fingerprint}" f"{_OPERATOR_STATUS_MARKER_SUFFIX}" ) def operator_status_marker(fingerprint: str) -> str: """Public form of :func:`_operator_status_marker`. Tests and any external caller that needs to construct or recognise a marker should import this rather than the underscore-prefixed helper. """ return _operator_status_marker(fingerprint) # ─── Implementer-side marker family (Phase 5b) ────────────────────────────── # # Distinct prefix from the reviewer namespace so the dedup logic # keyed off either prefix cannot mis-match a comment from the other # pipeline. _IMPLEMENTER_STATUS_MARKER_PREFIX = ( "" IMPLEMENTER_STATUS_MARKER_PREFIX = _IMPLEMENTER_STATUS_MARKER_PREFIX IMPLEMENTER_STATUS_MARKER_SUFFIX = _IMPLEMENTER_STATUS_MARKER_SUFFIX def implementer_status_marker(fingerprint: str) -> str: """Public form of the implementer-side fingerprint marker. Mirrors :func:`operator_status_marker` on the reviewer side. """ return ( f"{_IMPLEMENTER_STATUS_MARKER_PREFIX}" f"{fingerprint}" f"{_IMPLEMENTER_STATUS_MARKER_SUFFIX}" ) # ─── Detail sanitisation + fingerprinting ─────────────────────────────────── # Cap on the action_reason / detail string we surface in the # operator-status body. Large reasons (a verbose Forgejo error blob, # a 5 KB Python traceback excerpt) hurt readability and inflate the # fingerprint surface area. _STATUS_DETAIL_MAX_CHARS = 600 def _sanitize_status_detail(detail: str) -> str: """Normalise + cap the action_reason string for operator display. - Collapse runs of whitespace (newlines, tabs) into single spaces so a multi-line traceback doesn't span the operator comment. - Strip control characters (other than tab/newline) for log hygiene. - Cap at :data:`_STATUS_DETAIL_MAX_CHARS` characters with an ellipsis. Without the cap a runaway worker error message could blow past the comment-body limit. Used both for fingerprint computation (so stylistic whitespace differences don't fork a new fingerprint) and for the rendered body. Returns the empty string when ``detail`` is empty. """ if not detail: return "" cleaned = "".join( ch for ch in detail if ch == "\n" or ch == "\t" or (0x20 <= ord(ch) < 0x7F) or ord(ch) >= 0xA0 ) cleaned = re.sub(r"\s+", " ", cleaned).strip() if len(cleaned) > _STATUS_DETAIL_MAX_CHARS: cleaned = cleaned[: _STATUS_DETAIL_MAX_CHARS - 1] + "\u2026" return cleaned def _compute_status_fingerprint(action: str, detail: str) -> str: """Compute the 12-hex-char idempotency fingerprint for a status comment. Built from ``sha256(f"{action}|{sanitised_detail}")[:12]`` so two cycles producing the same outcome surface the same fingerprint and skip the duplicate post; distinct outcomes get distinct fingerprints and distinct posts. The ``action`` argument is the pipeline-specific verb (``failed`` / ``stale`` / ``rebase-failed`` / ``timeout`` / etc.); the ``detail`` argument is whatever free-form reason string the dispatcher captured. """ canonical = f"{action}|{_sanitize_status_detail(detail)}" return hashlib.sha256(canonical.encode("utf-8", errors="replace")).hexdigest()[:12] def _has_existing_operator_status( pr_comments: list[dict[str, Any]] | None, fingerprint: str, *, marker_prefix: str, ) -> bool: """Return True iff any prefetched PR comment carries the marker for ``fingerprint``. Used to short-circuit the operator-status POST so identical-outcome cycles don't double-post. ``marker_prefix`` is required (no default) so that a third pipeline cannot accidentally key against the reviewer's namespace. Reviewer callers pass :data:`_OPERATOR_STATUS_MARKER_PREFIX`; implementer callers pass :data:`_IMPLEMENTER_STATUS_MARKER_PREFIX`; each pipeline's idempotency stays scoped to its own namespace. Tolerates malformed entries: any comment whose ``body`` is not a string is skipped silently. """ if not pr_comments: return False needle = f"{marker_prefix}{fingerprint}" for comment in pr_comments: if not isinstance(comment, dict): continue body = comment.get("body") if isinstance(body, str) and needle in body: return True return False __all__ = ( "IMPLEMENTER_STATUS_MARKER_PREFIX", "IMPLEMENTER_STATUS_MARKER_SUFFIX", "OPERATOR_STATUS_MARKER_PREFIX", "OPERATOR_STATUS_MARKER_SUFFIX", "_compute_status_fingerprint", "_has_existing_operator_status", "_operator_status_marker", "_sanitize_status_detail", "implementer_status_marker", "operator_status_marker", )