diff --git a/app/routers/runs.py b/app/routers/runs.py
index 643c691..e437cf1 100644
--- a/app/routers/runs.py
+++ b/app/routers/runs.py
@@ -178,6 +178,7 @@ async def get_run(
# Try to load the recipe to show the plan
plan = None
+ plan_sexp = None # Native S-expression if available
recipe_id = run.get("recipe")
if recipe_id and len(recipe_id) == 64: # Looks like a hash
try:
@@ -185,7 +186,11 @@ async def get_run(
recipe_service = RecipeService(get_redis_client(), get_cache_manager())
recipe = await recipe_service.get_recipe(recipe_id)
if recipe:
- # Use the new build_dag method if available
+ # Use native S-expression if available (code is data!)
+ if recipe.get("sexp"):
+ plan_sexp = recipe["sexp"]
+
+ # Build steps for DAG visualization
dag = recipe.get("dag", {})
nodes = dag.get("nodes", [])
@@ -311,8 +316,9 @@ async def get_run(
}
})
- # Generate S-expression representation of the plan
- plan_sexp = plan_to_sexp(plan, run.get("recipe_name"))
+ # Use native S-expression if available, otherwise generate from plan
+ if not plan_sexp and plan:
+ plan_sexp = plan_to_sexp(plan, run.get("recipe_name"))
templates = get_templates(request)
return render(templates, "runs/detail.html", request,
diff --git a/app/services/run_service.py b/app/services/run_service.py
index 207e5e9..4eb84b7 100644
--- a/app/services/run_service.py
+++ b/app/services/run_service.py
@@ -482,10 +482,28 @@ class RunService:
async def get_run_plan(self, run_id: str) -> Optional[Dict[str, Any]]:
"""Get execution plan for a run."""
- plan_path = self.cache_dir / "plans" / f"{run_id}.json"
- if plan_path.exists():
- with open(plan_path) as f:
- return json.load(f)
+ # Prefer S-expression plan
+ sexp_path = self.cache_dir / "plans" / f"{run_id}.sexp"
+ if sexp_path.exists():
+ with open(sexp_path) as f:
+ return {"sexp": f.read(), "format": "sexp"}
+
+ # Fall back to JSON for legacy plans
+ json_path = self.cache_dir / "plans" / f"{run_id}.json"
+ if json_path.exists():
+ with open(json_path) as f:
+ plan = json.load(f)
+ plan["format"] = "json"
+ return plan
+
+ return None
+
+ async def get_run_plan_sexp(self, run_id: str) -> Optional[str]:
+ """Get execution plan as S-expression string."""
+ sexp_path = self.cache_dir / "plans" / f"{run_id}.sexp"
+ if sexp_path.exists():
+ with open(sexp_path) as f:
+ return f.read()
return None
async def get_run_artifacts(self, run_id: str) -> List[Dict[str, Any]]:
diff --git a/app/templates/runs/detail.html b/app/templates/runs/detail.html
index b2eb06a..f0a87ed 100644
--- a/app/templates/runs/detail.html
+++ b/app/templates/runs/detail.html
@@ -156,15 +156,17 @@
{% endfor %}
-
+
+ {% if plan_sexp %}
- Plan (S-expression)
+ Recipe (S-expression)
+ {% endif %}