diff --git a/.github/assets/passli-repo-banner.png b/.github/assets/passli-repo-banner.png
index 859f51b..816f64d 100644
Binary files a/.github/assets/passli-repo-banner.png and b/.github/assets/passli-repo-banner.png differ
diff --git a/README.md b/README.md
index f166e88..ce5b845 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@
- Interactive Demo •
The Problem •
Capabilities •
Architecture •
@@ -26,14 +25,6 @@
---
-## 🎬 Ephemeral Protocol in Action
-
-
-
-
-
----
-
## 🔒 The Problem Passli Solves
When sharing sensitive records with doctors, employers, financial institutions, or rental agencies, users are traditionally forced to:
@@ -48,18 +39,8 @@ When sharing sensitive records with doctors, employers, financial institutions,
## ✨ Core Platform Capabilities
### 🗄️ 1. Personal Document Vault
-
-
-
-
-
- **Domain Taxonomy**: Organize records into structured categories (`Medical`, `Education`, `Vehicle`, `Personal & Identity`, `Professional`, `Other`).
- **Encrypted Document Browser**: Filterable vault with instant search and cryptographic SHA-256 integrity inspection.
-
-
-
-
-
- **Storage Migration CLI**: Includes `python manage.py encrypt_vault_storage` for seamless batch migration of legacy storage records.
### 🔑 2. Cryptographic Share Passes & Ephemeral QR Protocol
@@ -113,10 +94,6 @@ When sharing sensitive records with doctors, employers, financial institutions,
## 🔐 Cryptographic Architecture Flow
-
-
-
-
```mermaid
flowchart TD
subgraph Ingestion["Document Upload & Storage"]
diff --git a/scripts/generate_banner.py b/scripts/generate_banner.py
new file mode 100644
index 0000000..97a599a
--- /dev/null
+++ b/scripts/generate_banner.py
@@ -0,0 +1,347 @@
+"""
+Passli — Professional Open-Source README Banner Generator
+=========================================================
+Produces a world-class, human-crafted repository identity banner at exactly 1376×768 PNG.
+
+Design Philosophy (Anti-AI-Slop, High-Craft Engineering)
+-------------------------------------------------------
+- Built with Passli's authentic visual identity and official brand tokens:
+ * Obsidian Void: #080C14
+ * Deep Slate: #0E1526
+ * Emerald Accent: #10B981
+ * Cyan Accent: #06B6D4
+ * Violet Accent: #8B5CF6
+- Incorporates the official Passli vector-rendered cyber-shield mark (`passli-app-icon.png`)
+- Clean typographic hierarchy with geometric alignment
+- Hairline-bordered glassmorphic feature badges with dual-tone dot indicators
+- Subtle technical telemetry (crosshairs, coordinate labels, status bar)
+- 2x supersampled anti-aliased rendering (2752×1536 -> 1376×768 Lanczos)
+- 100% deterministic, zero network/server dependency, pure Pillow
+"""
+
+from __future__ import annotations
+
+import math
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+# ---------------------------------------------------------------------------
+# Brand Color Tokens
+# ---------------------------------------------------------------------------
+BG_OBSIDIAN = (8, 12, 20) # #080C14 Base background
+BG_PANEL = (14, 21, 38) # #0E1526 Inner frame background
+BG_CHIP = (17, 24, 39) # #111827 Chip fill
+BORDER_MUTED = (30, 41, 59) # #1E293B Subtle hairline border
+BORDER_HIGHLIGHT = (51, 65, 85) # #334155 Frame highlight border
+
+COLOR_EMERALD = (16, 185, 129) # #10B981 Brand Primary
+COLOR_CYAN = (6, 182, 212) # #06B6D4 Brand Secondary
+COLOR_VIOLET = (139, 92, 246) # #8B5CF6 Crypto / AAD
+COLOR_AMBER = (245, 158, 11) # #F59E0B Ephemeral timer
+COLOR_TEXT_WHITE = (248, 250, 252) # #F8FAFC Heading White
+COLOR_TEXT_MUTED = (148, 163, 184) # #94A3B8 Secondary Text
+COLOR_TEXT_DIM = (100, 116, 139) # #64748B Dim labels
+
+# Output dimensions enforced by test_repo_visual_identity
+OUT_W, OUT_H = 1376, 768
+SCALE = 2
+CANVAS_W, CANVAS_H = OUT_W * SCALE, OUT_H * SCALE
+
+ASSETS_DIR = Path(".github/assets")
+ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+BRAND_ICON_PATH = Path("static/img/brand/passli-app-icon.png")
+
+
+# ---------------------------------------------------------------------------
+# Font Discovery
+# ---------------------------------------------------------------------------
+def _pick_font(candidates: list[str]) -> str | None:
+ for c in candidates:
+ if Path(c).exists():
+ return c
+ return None
+
+
+FONT_BOLD = _pick_font([
+ "C:/Windows/Fonts/segoeuib.ttf",
+ "C:/Windows/Fonts/arialbd.ttf",
+ "C:/Windows/Fonts/calibrib.ttf",
+])
+FONT_SEMIBOLD = _pick_font([
+ "C:/Windows/Fonts/seguisb.ttf",
+ "C:/Windows/Fonts/segoeuib.ttf",
+ "C:/Windows/Fonts/arialbd.ttf",
+])
+FONT_REGULAR = _pick_font([
+ "C:/Windows/Fonts/segoeui.ttf",
+ "C:/Windows/Fonts/arial.ttf",
+ "C:/Windows/Fonts/calibri.ttf",
+])
+FONT_MONO = _pick_font([
+ "C:/Windows/Fonts/consola.ttf",
+ "C:/Windows/Fonts/cascadiacode.ttf",
+ "C:/Windows/Fonts/cour.ttf",
+])
+
+
+def get_font(path: str | None, size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
+ if path and Path(path).exists():
+ try:
+ return ImageFont.truetype(path, size)
+ except Exception:
+ pass
+ return ImageFont.load_default()
+
+
+# ---------------------------------------------------------------------------
+# Drawing Utilities
+# ---------------------------------------------------------------------------
+def draw_radial_ambient_glow(canvas: Image.Image, cx: int, cy: int, max_radius: int) -> None:
+ """Renders a soft, diffuse ambient glow centered behind the main brand mark."""
+ glow_layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
+ draw = ImageDraw.Draw(glow_layer)
+
+ steps = 45
+ for i in range(steps, 0, -1):
+ ratio = i / steps
+ r = int(max_radius * ratio)
+ alpha = int(22 * (1.0 - math.pow(ratio, 0.7)))
+ if ratio < 0.4:
+ color = (6, 182, 212, alpha)
+ elif ratio < 0.7:
+ color = (99, 102, 241, alpha)
+ else:
+ color = (16, 185, 129, alpha)
+ draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=color)
+
+ canvas.alpha_composite(glow_layer)
+
+
+def draw_cyber_grid(draw: ImageDraw.ImageDraw, x0: int, y0: int, x1: int, y1: int, step: int = 48) -> None:
+ """Subtle matrix grid with micro-dots at intersections."""
+ grid_color = (18, 26, 44)
+ dot_color = (30, 42, 68)
+
+ for x in range(x0, x1, step):
+ draw.line([x, y0, x, y1], fill=grid_color, width=1)
+ for y in range(y0, y1, step):
+ draw.line([x0, y, x1, y], fill=grid_color, width=1)
+
+ for x in range(x0, x1, step):
+ for y in range(y0, y1, step):
+ draw.rectangle([x - 1, y - 1, x + 1, y + 1], fill=dot_color)
+
+
+def draw_tech_crosshair(draw: ImageDraw.ImageDraw, cx: int, cy: int, arm: int = 12, color=BORDER_HIGHLIGHT) -> None:
+ """Draws a refined + crosshair marker."""
+ draw.line([cx - arm, cy, cx + arm, cy], fill=color, width=2)
+ draw.line([cx, cy - arm, cx, cy + arm], fill=color, width=2)
+
+
+def draw_pill_badge(
+ draw: ImageDraw.ImageDraw,
+ cx: int,
+ cy: int,
+ text: str,
+ font: ImageFont.FreeTypeFont,
+ accent_color: tuple[int, int, int],
+) -> int:
+ """Draws a modern, pill-shaped badge with a glowing active dot and hairline border."""
+ bbox = draw.textbbox((0, 0), text, font=font)
+ text_w = bbox[2] - bbox[0]
+ text_h = bbox[3] - bbox[1]
+
+ dot_radius = 4 * SCALE
+ dot_gap = 10 * SCALE
+ pad_x = 18 * SCALE
+ pad_y = 9 * SCALE
+
+ pill_w = pad_x * 2 + dot_radius * 2 + dot_gap + text_w
+ pill_h = text_h + pad_y * 2
+ x0 = cx - pill_w // 2
+ y0 = cy - pill_h // 2
+ x1 = x0 + pill_w
+ y1 = y0 + pill_h
+
+ draw.rounded_rectangle([x0, y0, x1, y1], radius=pill_h // 2, fill=BG_CHIP, outline=BORDER_MUTED, width=SCALE)
+
+ dot_cx = x0 + pad_x + dot_radius
+ dot_cy = cy
+ draw.ellipse(
+ [dot_cx - dot_radius - 2, dot_cy - dot_radius - 2, dot_cx + dot_radius + 2, dot_cy + dot_radius + 2],
+ fill=None,
+ outline=(*accent_color, 90),
+ width=1,
+ )
+ draw.ellipse(
+ [dot_cx - dot_radius, dot_cy - dot_radius, dot_cx + dot_radius, dot_cy + dot_radius],
+ fill=accent_color,
+ )
+
+ text_x = dot_cx + dot_radius + dot_gap
+ text_y = cy - text_h // 2 - 1
+ draw.text((text_x, text_y), text, font=font, fill=COLOR_TEXT_WHITE)
+
+ return pill_w
+
+
+# ---------------------------------------------------------------------------
+# Main Banner Generation
+# ---------------------------------------------------------------------------
+def generate_identity_banner() -> Path:
+ print(f"Generating professional open-source banner ({CANVAS_W}x{CANVAS_H} supersampled -> {OUT_W}x{OUT_H})...")
+
+ banner_rgba = Image.new("RGBA", (CANVAS_W, CANVAS_H), (*BG_OBSIDIAN, 255))
+ draw = ImageDraw.Draw(banner_rgba)
+
+ f_kicker = get_font(FONT_MONO, 13 * SCALE)
+ f_title = get_font(FONT_BOLD, 74 * SCALE)
+ f_tagline = get_font(FONT_SEMIBOLD, 22 * SCALE)
+ f_subtext = get_font(FONT_REGULAR, 15 * SCALE)
+ f_badge = get_font(FONT_MONO, 12 * SCALE)
+ f_telemetry = get_font(FONT_MONO, 11 * SCALE)
+
+ pad_outer_x = 44 * SCALE
+ pad_outer_y = 36 * SCALE
+ frame_x0 = pad_outer_x
+ frame_y0 = pad_outer_y
+ frame_x1 = CANVAS_W - pad_outer_x
+ frame_y1 = CANVAS_H - pad_outer_y
+ frame_radius = 20 * SCALE
+
+ # 1. Subtle Cyber Matrix Grid within the main frame
+ draw_cyber_grid(draw, frame_x0 + 16, frame_y0 + 16, frame_x1 - 16, frame_y1 - 16, step=44 * SCALE)
+
+ # 2. Main Framed Card
+ draw.rounded_rectangle(
+ [frame_x0, frame_y0, frame_x1, frame_y1],
+ radius=frame_radius,
+ fill=(*BG_PANEL, 210),
+ outline=BORDER_MUTED,
+ width=SCALE,
+ )
+
+ # Top highlight border accent
+ draw.line(
+ [frame_x0 + frame_radius, frame_y0, frame_x1 - frame_radius, frame_y0],
+ fill=BORDER_HIGHLIGHT,
+ width=SCALE,
+ )
+
+ # 3. Technical Corner Crosshairs
+ crosshair_margin = 18 * SCALE
+ draw_tech_crosshair(draw, frame_x0 + crosshair_margin, frame_y0 + crosshair_margin, arm=7 * SCALE)
+ draw_tech_crosshair(draw, frame_x1 - crosshair_margin, frame_y0 + crosshair_margin, arm=7 * SCALE)
+ draw_tech_crosshair(draw, frame_x0 + crosshair_margin, frame_y1 - crosshair_margin, arm=7 * SCALE)
+ draw_tech_crosshair(draw, frame_x1 - crosshair_margin, frame_y1 - crosshair_margin, arm=7 * SCALE)
+
+ # 4. Top Telemetry Kicker Bar
+ kicker_y = frame_y0 + 38 * SCALE
+ kicker_text = "PASSLI SECURITY PROTOCOL // OPEN-SOURCE DOCUMENT VAULT & EPHEMERAL SHARING"
+ kicker_bbox = draw.textbbox((0, 0), kicker_text, font=f_kicker)
+ kicker_w = kicker_bbox[2] - kicker_bbox[0]
+ draw.text((CANVAS_W // 2 - kicker_w // 2, kicker_y), kicker_text, font=f_kicker, fill=COLOR_CYAN)
+
+ draw.line(
+ [CANVAS_W // 2 - 320 * SCALE, kicker_y + 22 * SCALE, CANVAS_W // 2 + 320 * SCALE, kicker_y + 22 * SCALE],
+ fill=BORDER_MUTED,
+ width=1,
+ )
+
+ # 5. Ambient Glow behind the Official Brand Mark
+ icon_center_x = CANVAS_W // 2
+ icon_center_y = int(frame_y0 + 175 * SCALE)
+ draw_radial_ambient_glow(banner_rgba, icon_center_x, icon_center_y, max_radius=190 * SCALE)
+ draw = ImageDraw.Draw(banner_rgba)
+
+ # 6. Official Passli Cyber-Shield App Icon
+ if BRAND_ICON_PATH.exists():
+ raw_icon = Image.open(BRAND_ICON_PATH).convert("RGBA")
+ target_icon_size = 196 * SCALE
+ icon_resized = raw_icon.resize((target_icon_size, target_icon_size), Image.Resampling.LANCZOS)
+ icon_paste_x = icon_center_x - target_icon_size // 2
+ icon_paste_y = icon_center_y - target_icon_size // 2
+ banner_rgba.alpha_composite(icon_resized, (icon_paste_x, icon_paste_y))
+ draw = ImageDraw.Draw(banner_rgba)
+
+ # 7. Wordmark "Passli"
+ title_text = "Passli"
+ title_bbox = draw.textbbox((0, 0), title_text, font=f_title)
+ title_w = title_bbox[2] - title_bbox[0]
+ title_h = title_bbox[3] - title_bbox[1]
+ title_y = int(icon_center_y + 115 * SCALE)
+ title_x = CANVAS_W // 2 - title_w // 2
+ draw.text((title_x, title_y), title_text, font=f_title, fill=COLOR_TEXT_WHITE)
+
+ # 8. Primary Tagline
+ tagline_text = "Cryptographic Document Vault * Ephemeral Sharing Platform"
+ tagline_bbox = draw.textbbox((0, 0), tagline_text, font=f_tagline)
+ tagline_w = tagline_bbox[2] - tagline_bbox[0]
+ tagline_y = title_y + title_h + 16 * SCALE
+ draw.text((CANVAS_W // 2 - tagline_w // 2, tagline_y), tagline_text, font=f_tagline, fill=COLOR_EMERALD)
+
+ # 9. Descriptive Mission Pitch
+ subtext = "Zero-knowledge recipient sharing, AES-256-GCM encryption at rest & time-gated ephemeral passes"
+ subtext_bbox = draw.textbbox((0, 0), subtext, font=f_subtext)
+ subtext_w = subtext_bbox[2] - subtext_bbox[0]
+ subtext_y = tagline_y + 36 * SCALE
+ draw.text((CANVAS_W // 2 - subtext_w // 2, subtext_y), subtext, font=f_subtext, fill=COLOR_TEXT_MUTED)
+
+ # 10. Feature Badges Row (4 Glassmorphic Hairline Chips)
+ badges = [
+ ("AES-256-GCM AEAD", COLOR_EMERALD),
+ ("PBKDF2-HMAC-SHA256", COLOR_CYAN),
+ ("Zero Disk Decryption", COLOR_VIOLET),
+ ("Time-Gated QR Passes", COLOR_AMBER),
+ ]
+
+ badge_y = subtext_y + 54 * SCALE
+ gap = 14 * SCALE
+
+ badge_widths = []
+ for text, _ in badges:
+ bb = draw.textbbox((0, 0), text, font=f_badge)
+ tw = bb[2] - bb[0]
+ pill_w = (18 * SCALE) * 2 + (4 * SCALE) * 2 + (10 * SCALE) + tw
+ badge_widths.append(pill_w)
+
+ total_badges_w = sum(badge_widths) + gap * (len(badges) - 1)
+ cur_x = CANVAS_W // 2 - total_badges_w // 2
+
+ for (text, accent), w in zip(badges, badge_widths):
+ badge_cx = cur_x + w // 2
+ draw_pill_badge(draw, badge_cx, badge_y, text, f_badge, accent)
+ cur_x += w + gap
+
+ # 11. Bottom Architectural Status Bar
+ bar_y = frame_y1 - 38 * SCALE
+ draw.line(
+ [frame_x0 + 24 * SCALE, bar_y - 12 * SCALE, frame_x1 - 24 * SCALE, bar_y - 12 * SCALE],
+ fill=BORDER_MUTED,
+ width=1,
+ )
+
+ left_meta = "STATUS: PRODUCTION READY // REPO: GITHUB.COM/ALINAWAZCODE/PASSLI"
+ draw.text((frame_x0 + 28 * SCALE, bar_y), left_meta, font=f_telemetry, fill=COLOR_TEXT_DIM)
+
+ right_meta = "PYTHON 3.12 * DJANGO 5.1 * ZERO PERSISTED PLAINTEXT * MIT LICENSE"
+ rm_bbox = draw.textbbox((0, 0), right_meta, font=f_telemetry)
+ rm_w = rm_bbox[2] - rm_bbox[0]
+ draw.text((frame_x1 - 28 * SCALE - rm_w, bar_y), right_meta, font=f_telemetry, fill=COLOR_TEXT_DIM)
+
+ # ---------------------------------------------------------------------------
+ # Downsample via Lanczos to target 1376x768
+ # ---------------------------------------------------------------------------
+ final_rgb = banner_rgba.convert("RGB")
+ final_output = final_rgb.resize((OUT_W, OUT_H), Image.Resampling.LANCZOS)
+
+ out_file = ASSETS_DIR / "passli-repo-banner.png"
+ final_output.save(str(out_file), "PNG", optimize=True)
+ file_size_kb = out_file.stat().st_size / 1024
+ print(f"SUCCESS: Saved {out_file} ({OUT_W}x{OUT_H}, {file_size_kb:.1f} KB)")
+ return out_file
+
+
+if __name__ == "__main__":
+ generate_identity_banner()
diff --git a/scripts/generate_official_repo_assets.py b/scripts/generate_official_repo_assets.py
index 6303283..a4443d7 100644
--- a/scripts/generate_official_repo_assets.py
+++ b/scripts/generate_official_repo_assets.py
@@ -1,11 +1,12 @@
-import os
import io
-import time
+import os
from pathlib import Path
-from PIL import Image, ImageDraw, ImageFont, ImageFilter
+import time
+
+from PIL import Image, ImageDraw, ImageFont
from selenium import webdriver
-from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
BASE_URL = "http://127.0.0.1:8000"
@@ -205,84 +206,11 @@ def snap(count=1):
print(f"Saved real passli-ephemeral-workflow.gif: {len(processed_frames)} frames, {os.path.getsize(out_gif)} bytes")
-def build_authentic_hero_banner(dash_raw_img):
- print("3. Building authentic Hero Banner featuring the REAL Passli application...")
- BANNER_W, BANNER_H = 1376, 768
-
- banner = Image.new("RGB", (BANNER_W, BANNER_H), (11, 15, 23)) # #0B0F17
- draw = ImageDraw.Draw(banner)
-
- # Subtle background cyber grid
- grid_step = 40
- for x in range(0, BANNER_W, grid_step):
- draw.line([x, 0, x, BANNER_H], fill=(17, 24, 39), width=1)
- for y in range(0, BANNER_H, grid_step):
- draw.line([0, y, BANNER_W, y], fill=(17, 24, 39), width=1)
-
- # Ambient glows
- # Load fonts
- try:
- font_brand = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 36)
- font_tag = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 18)
- font_badge = ImageFont.truetype("C:/Windows/Fonts/segoeuib.ttf", 13)
- except Exception:
- font_brand = font_tag = font_badge = ImageFont.load_default()
-
- # Brand Header Area
- # Passli Shield Icon
- sx, sy = 60, 48
- draw.polygon(
- [(sx + 16, sy), (sx + 32, sy + 6), (sx + 32, sy + 22), (sx + 16, sy + 34), (sx, sy + 22), (sx, sy + 6)],
- outline=(16, 185, 129),
- fill=(16, 185, 129, 40),
- )
- draw.ellipse([(sx + 12, sy + 12), (sx + 20, sy + 20)], fill=(16, 185, 129))
-
- draw.text((sx + 44, sy - 4), "Passli", font=font_brand, fill=(248, 250, 252))
- draw.text(
- (sx + 180, sy + 10), "• Cryptographic Document Vault & Ephemeral Sharing", font=font_tag, fill=(148, 163, 184)
- )
-
- # Badges on right side of banner header
- bx = BANNER_W - 520
- badges = [
- ("AES-256-GCM AEAD", (16, 185, 129)),
- ("PBKDF2-HMAC-SHA256", (6, 182, 212)),
- ("Zero Disk Decryption", (168, 85, 247)),
- ]
- for b_text, b_color in badges:
- bw = int(draw.textlength(b_text, font=font_badge)) + 22
- draw.rounded_rectangle([bx, sy + 4, bx + bw, sy + 32], radius=6, fill=(17, 24, 39), outline=b_color, width=1)
- draw.text((bx + 11, sy + 9), b_text, font=font_badge, fill=b_color)
- bx += bw + 12
-
- # Main Showcase Window: Framed screenshot of the ACTUAL site dashboard
- # Scale real dashboard to fit elegantly inside banner
- mw = 1256
- mh = 630
- dash_framed = frame_browser_window(dash_raw_img, "https://passli.dev/dashboard/")
- # Downscale smoothly
- dash_scaled = dash_framed.resize((mw, int(dash_framed.height * (mw / dash_framed.width))), Image.Resampling.LANCZOS)
-
- # Crop to fit inside banner height
- dash_cropped = dash_scaled.crop((0, 0, mw, mh))
-
- # Subtle window shadow
- win_x = (BANNER_W - mw) // 2
- win_y = 115
- draw.rounded_rectangle(
- [win_x - 2, win_y - 2, win_x + mw + 2, win_y + mh + 2],
- radius=10,
- fill=(16, 185, 129, 30),
- outline=(56, 189, 248),
- width=1,
- )
-
- banner.paste(dash_cropped, (win_x, win_y))
+def build_authentic_hero_banner(dash_raw_img=None):
+ print("3. Building authentic Hero Banner featuring the Passli visual identity...")
+ from scripts.generate_banner import generate_identity_banner
- out_banner = ASSETS_DIR / "passli-repo-banner.png"
- banner.save(str(out_banner), "PNG", optimize=True)
- print(f"Saved authentic passli-repo-banner.png ({BANNER_W}x{BANNER_H}, {os.path.getsize(out_banner)} bytes)")
+ return generate_identity_banner()
if __name__ == "__main__":
diff --git a/tests/unit/test_repo_visual_identity.py b/tests/unit/test_repo_visual_identity.py
index 4ff409a..528c70f 100644
--- a/tests/unit/test_repo_visual_identity.py
+++ b/tests/unit/test_repo_visual_identity.py
@@ -80,21 +80,39 @@ def test_vault_documents_asset(self):
self.assertEqual(im.size, (1376, 768))
def test_readme_references_assets(self):
- """README.md must reference the visual identity assets via valid relative paths."""
+ """README.md must reference only the primary unique identification banner."""
+ import re
+
self.assertTrue(self.readme_path.exists(), "README.md must exist")
content = self.readme_path.read_text(encoding="utf-8")
- required_assets = [
- ".github/assets/passli-repo-banner.png",
+ # Primary unique identification hero banner must be referenced and exist
+ primary_banner = ".github/assets/passli-repo-banner.png"
+ self.assertIn(primary_banner, content, f"README.md must contain relative link to {primary_banner}")
+ self.assertTrue((self.base_dir / primary_banner).exists(), f"Asset {primary_banner} must exist on disk")
+
+ # Extract all image tags (HTML
and Markdown )
+ html_imgs = re.findall(r'
]*src=[\x22\x27]([^\x22\x27]+)[\x22\x27]', content)
+ md_imgs = re.findall(r'!\[[^\]]*\]\(([^)]+)\)', content)
+ all_imgs = html_imgs + md_imgs
+
+ # Local asset images embedded in README must ONLY consist of the primary banner
+ local_asset_imgs = [img for img in all_imgs if ".github/assets" in img or not img.startswith("http")]
+ self.assertEqual(
+ local_asset_imgs,
+ [primary_banner],
+ f"README.md must contain only the primary banner as embedded local asset, found: {local_asset_imgs}",
+ )
+
+ # Secondary visual embeds must not be embedded anywhere in README.md
+ secondary_assets = [
".github/assets/passli-ephemeral-workflow.gif",
".github/assets/passli-dashboard-preview.png",
".github/assets/passli-vault-documents.png",
".github/assets/passli-cryptographic-pipeline.png",
]
- for asset in required_assets:
- self.assertIn(asset, content, f"README.md must contain relative link to {asset}")
- target_file = self.base_dir / asset
- self.assertTrue(target_file.exists(), f"Asset {asset} referenced in README.md must exist")
+ for asset in secondary_assets:
+ self.assertNotIn(asset, content, f"README.md should not embed secondary asset {asset}")
def test_architecture_doc_references_assets(self):
"""docs/ARCHITECTURE.md must reference architecture and workflow assets."""
@@ -114,6 +132,7 @@ def test_branding_doc_exists_and_references_assets(self):
self.assertIn("passli-repo-banner.png", content)
self.assertIn("passli-ephemeral-workflow.gif", content)
self.assertIn("passli-dashboard-preview.png", content)
+ self.assertIn("passli-vault-documents.png", content)
self.assertIn("passli-cryptographic-pipeline.png", content)
def test_asset_catalog_doc_references_all_assets(self):
@@ -125,14 +144,17 @@ def test_asset_catalog_doc_references_all_assets(self):
"passli-repo-banner.png",
"passli-ephemeral-workflow.gif",
"passli-dashboard-preview.png",
+ "passli-vault-documents.png",
"passli-cryptographic-pipeline.png",
]:
self.assertIn(asset_filename, content)
self.assertTrue((self.assets_dir / asset_filename).exists())
def test_generator_scripts_exist(self):
- """Scripts to generate GIF and dashboard preview must exist and be accessible."""
+ """Scripts to generate GIF, dashboard preview, and official banner must exist and be accessible."""
gif_script = self.base_dir / "scripts" / "generate_ephemeral_flow_gif.py"
dash_script = self.base_dir / "scripts" / "generate_dashboard_preview.py"
+ official_script = self.base_dir / "scripts" / "generate_official_repo_assets.py"
self.assertTrue(gif_script.exists(), "generate_ephemeral_flow_gif.py must exist")
self.assertTrue(dash_script.exists(), "generate_dashboard_preview.py must exist")
+ self.assertTrue(official_script.exists(), "generate_official_repo_assets.py must exist")