#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" # dependencies = [ # "openai>=1.100.2", # "python-dotenv>=1.1.0", # "typer>=0.9.0", # ] # /// """Build and generate a non-gating BM Bossbot PR image.""" from __future__ import annotations import hashlib import html import json import re from dataclasses import dataclass from enum import StrEnum from pathlib import Path from typing import Annotated, Any, Mapping import typer if __package__: from .generate_infographic import ( DEFAULT_MODEL, DEFAULT_QUALITY, DEFAULT_SIZE, generate_image_result, ) else: from generate_infographic import ( DEFAULT_MODEL, DEFAULT_QUALITY, DEFAULT_SIZE, generate_image_result, ) SUMMARY_START = "" SUMMARY_END = "" THEME_START = "" THEME_END = "" PROVENANCE_START = "" PROVENANCE_END = "" IMAGE_START = "" IMAGE_END = "" # Managed blocks are bot-written artifacts (review verdict, image embed, # provenance). They must never feed the image: sourcing the review summary is # what made every image an "APPROVED" stamp instead of depicting the change. MANAGED_BLOCKS = ( (SUMMARY_START, SUMMARY_END), (THEME_START, THEME_END), (PROVENANCE_START, PROVENANCE_END), (IMAGE_START, IMAGE_END), ) app = typer.Typer( add_completion=False, help="Generate a non-gating BM Bossbot PR image.", no_args_is_help=True, ) class ThemeSource(StrEnum): AUTO = "auto" CLI = "cli" PR_BODY = "pr-body" @dataclass(frozen=True) class ThemeSelection: theme: str source: ThemeSource BM_IMAGE_THEME_POOL = ( "computer science college textbook: SICP-style diagrams, automata, compiler " "pipelines, type theory, and annotated chalkboard rigor", "classic literature: sea voyages, gothic manors, Dickensian streets, library " "marginalia, and travel-journal artifacts", "fantasy quest ledger: original guild maps, spellbooks, dungeon keys, tavern " "notices, and artifact inventories with no copyrighted settings", "heavy music editorial: metal, hard rock, punk, techno, soul, or reggae " "tour-poster energy with no direct band logos or likenesses", "knockoff space opera: fleet routes, mission consoles, contraband manifests, " "and practical starship drama with no named fictional universes", "sword-and-sorcery: ruined temples, desert roads, battle standards, ancient " "maps, and heroic silhouettes with no named character likenesses", "comic book cover: original splash-page composition, caption boxes, clean " "halftone texture, and bold issue-cover drama", "French new wave movie poster: stark typography, city streets, jump-cut " "composition, and high-contrast editorial photography cues", "WWII public-information poster: home-front logistics, mobilization arrows, " "bold simplified figures, and no real-world party symbols or hate imagery", "Italian movie poster: hand-painted drama, expressive color, credit-block " "energy, and 1960s or 1970s cinema composition with no actor likenesses", "Shakespearean stage: acts and scenes, court intrigue, stage blocking, " "dramatis personae, backstage cue sheets, and theatrical light", "Greek mythology: temple steps, oracle tablets, constellations, labyrinths, " "ship routes, and original heroic allegory", "noir detective photography: case files, typed evidence labels, civic " "infrastructure, streetlight shadows, and newsroom archive grit", "space exploration and astronomy: celestial atlases, observatory charts, " "orbital mechanics, planetary survey routes, and deep-space mission drama", "editorial painting: abstract, classical landscape, western action, " "chiaroscuro, historical mural, stormy seascape, or allegorical canvas", "classic black-and-white photography: documentary field report, contact " "sheet, street photography, civic infrastructure, and darkroom contrast", "80's action movie poster: smoky backlit warehouses, neon streets, practical " "explosions, mission dossiers, countdowns, and no actor likenesses", "alchemy manuscript: transformation diagrams, annotated symbols, recipe-like " "process artifacts, and illuminated margins", "brutalist civic planning: concrete signage, zoning blocks, transit diagrams, " "infrastructure maps, and stern public-service clarity", ) def build_change_shape( context: Mapping[str, Any], *, max_commits: int = 10, max_files: int = 10, ) -> str: """Render a compact factual digest of the PR: labels, linked issues, commits, files. Mirrors the delivery context the Basic Memory CI capture flow collects (ProjectUpdateContext): the goal is to ground the image in the theme of the WHOLE change — what it touches and why — not just the title/description. """ lines: list[str] = [] labels = [str(label.get("name", "")) for label in context.get("labels") or []] labels = [label for label in labels if label] if labels: lines.append(f"Labels: {', '.join(labels)}") issues = context.get("closingIssuesReferences") or [] if issues: lines.append("Linked issues:") for issue in issues: number = issue.get("number") title = str(issue.get("title") or "").strip() lines.append(f"- #{number}: {title}" if title else f"- #{number}") commits = context.get("commits") or [] subjects = [str(commit.get("messageHeadline") or "").strip() for commit in commits] # Merge commits carry no thematic signal — they're branch bookkeeping. subjects = [ subject for subject in subjects if subject and not subject.startswith(("Merge branch ", "Merge pull request ")) ] if subjects: lines.append(f"Commit subjects ({len(subjects)} total):") lines.extend(f"- {subject}" for subject in subjects[:max_commits]) if len(subjects) > max_commits: lines.append(f"- ... and {len(subjects) - max_commits} more") files = context.get("files") or [] if files: additions = sum(int(item.get("additions") or 0) for item in files) deletions = sum(int(item.get("deletions") or 0) for item in files) lines.append(f"Files changed ({len(files)} total, +{additions}/-{deletions}):") ranked = sorted( files, key=lambda item: int(item.get("additions") or 0) + int(item.get("deletions") or 0), reverse=True, ) for item in ranked[:max_files]: path = str(item.get("path") or "") lines.append(f"- {path} (+{item.get('additions') or 0}/-{item.get('deletions') or 0})") if len(files) > max_files: lines.append(f"- ... and {len(files) - max_files} more files") return "\n".join(lines) if lines else "(no additional change context available)" def extract_pr_content(pr_body: str) -> str: """Return the author's own PR description with all managed bot blocks removed.""" content = pr_body for start, end in MANAGED_BLOCKS: content = re.sub( rf"{re.escape(start)}.*?{re.escape(end)}", "", content, flags=re.DOTALL, ) return content.strip() def extract_infographic_theme(pr_body: str) -> str | None: pattern = re.compile( rf"{re.escape(THEME_START)}\s*(.*?)\s*{re.escape(THEME_END)}", flags=re.DOTALL, ) match = pattern.search(pr_body) if not match: return None theme = match.group(1).strip() return theme or None def select_image_theme( *, pr_number: int, pr_title: str, pr_body: str, theme_override: str | None, ) -> ThemeSelection: if theme_override: return ThemeSelection(theme=theme_override, source=ThemeSource.CLI) body_theme = extract_infographic_theme(pr_body) if body_theme: return ThemeSelection(theme=body_theme, source=ThemeSource.PR_BODY) # Seed on author-owned PR identity, not the review summary, so the pick is # stable across re-reviews of the same PR. seed = f"{pr_number}\n{pr_title}".encode("utf-8") index = int.from_bytes(hashlib.sha256(seed).digest()[:2], byteorder="big") % len( BM_IMAGE_THEME_POOL ) return ThemeSelection(theme=BM_IMAGE_THEME_POOL[index], source=ThemeSource.AUTO) def _preformatted(value: str) -> str: return f"
{html.escape(value, quote=False)}"
def build_infographic_provenance_block(
*,
pr_number: int,
output_path: Path,
model: str,
size: str,
quality: str,
theme: str,
theme_source: ThemeSource,
) -> str:
return f"""
{PROVENANCE_START}