From 0c7e43e069c93cb33f9a85d54b71daea533bec92 Mon Sep 17 00:00:00 2001 From: gilesb Date: Wed, 7 Jan 2026 14:29:40 +0000 Subject: [PATCH] feat: link effect to specific git commit for provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Capture effects repo commit hash at render time - Store effects_commit in run record - Effect URLs now link to exact commit, not main branch - Include commit in raw JSON provenance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server.py | 21 ++++++++++++++++++++- tasks.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index c873ace..d263c39 100644 --- a/server.py +++ b/server.py @@ -89,6 +89,7 @@ class RunStatus(BaseModel): output_hash: Optional[str] = None error: Optional[str] = None celery_task_id: Optional[str] = None + effects_commit: Optional[str] = None def file_hash(path: Path) -> str: @@ -268,6 +269,11 @@ async def get_run(run_id: str): run.completed_at = datetime.now(timezone.utc).isoformat() run.output_hash = result.get("output", {}).get("content_hash") + # Extract effects commit from provenance + effects = result.get("effects", []) + if effects: + run.effects_commit = effects[0].get("repo_commit") + # Cache the output output_path = Path(result.get("output", {}).get("local_path", "")) if output_path.exists(): @@ -641,6 +647,10 @@ async def ui_detail_page(run_id: str): run.status = "completed" run.completed_at = datetime.now(timezone.utc).isoformat() run.output_hash = result.get("output", {}).get("content_hash") + # Extract effects commit + effects = result.get("effects", []) + if effects: + run.effects_commit = effects[0].get("repo_commit") output_path = Path(result.get("output", {}).get("local_path", "")) if output_path.exists(): cache_file(output_path) @@ -649,7 +659,11 @@ async def ui_detail_page(run_id: str): run.error = str(task.result) save_run(run) - effect_url = f"https://git.rose-ash.com/art-dag/effects/src/branch/main/{run.recipe}" + # Build effect URL - use commit hash if available + if run.effects_commit and run.effects_commit != "unknown": + effect_url = f"https://git.rose-ash.com/art-dag/effects/src/commit/{run.effects_commit}/{run.recipe}" + else: + effect_url = f"https://git.rose-ash.com/art-dag/effects/src/branch/main/{run.recipe}" status_class = run.status html = f""" @@ -769,6 +783,7 @@ async def ui_detail_page(run_id: str): "run_id": run.run_id, "status": run.status, "recipe": run.recipe, + "effects_commit": run.effects_commit, "effect_url": effect_url, "inputs": run.inputs, "output_hash": run.output_hash, @@ -805,6 +820,10 @@ async def ui_run_partial(run_id: str): run.status = "completed" run.completed_at = datetime.now(timezone.utc).isoformat() run.output_hash = result.get("output", {}).get("content_hash") + # Extract effects commit + effects = result.get("effects", []) + if effects: + run.effects_commit = effects[0].get("repo_commit") output_path = Path(result.get("output", {}).get("local_path", "")) if output_path.exists(): cache_file(output_path) diff --git a/tasks.py b/tasks.py index b3095d7..8d339db 100644 --- a/tasks.py +++ b/tasks.py @@ -7,6 +7,7 @@ Distributed rendering tasks for the Art DAG system. import hashlib import json import os +import subprocess import sys from datetime import datetime, timezone from pathlib import Path @@ -16,6 +17,24 @@ from celery_app import app # Add effects to path (use env var in Docker, fallback to home dir locally) EFFECTS_PATH = Path(os.environ.get("EFFECTS_PATH", str(Path.home() / "artdag-effects"))) + + +def get_effects_commit() -> str: + """Get current git commit hash of effects repo.""" + try: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=EFFECTS_PATH, + capture_output=True, + text=True + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception: + pass + return "unknown" + + sys.path.insert(0, str(EFFECTS_PATH / "dog")) @@ -106,6 +125,9 @@ def render_effect(self, input_hash: str, effect_name: str, output_name: str) -> if output_hash != expected_hash: raise ValueError(f"Output hash mismatch: expected {expected_hash}, got {output_hash}") + # Get effects repo commit + effects_commit = get_effects_commit() + # Build provenance provenance = { "task_id": self.request.id, @@ -120,7 +142,12 @@ def render_effect(self, input_hash: str, effect_name: str, output_name: str) -> {"content_hash": input_hash} ], "effects": [ - {"name": f"effect:{effect_name}", "content_hash": REGISTRY[f"effect:{effect_name}"]["hash"]} + { + "name": f"effect:{effect_name}", + "content_hash": REGISTRY[f"effect:{effect_name}"]["hash"], + "repo_commit": effects_commit, + "repo_url": f"https://git.rose-ash.com/art-dag/effects/src/commit/{effects_commit}/{effect_name}" + } ], "infrastructure": { "software": {"name": "infra:artdag", "content_hash": REGISTRY["infra:artdag"]["hash"]},