From d14b9eafa785d085a82513cc78e7e003811ad93a Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 5 Jul 2026 15:24:18 -0700 Subject: [PATCH] feat: supabase stack seeds a `requires` on the substrate at create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stack's substrate dependency now becomes a real encoded `requires` at `castle program create` time (STACK_REQUIRES), so the relationship graph shows it. This keeps `stack` uncoupled from the runtime model — the stack declares the edge once at creation; the graph only ever reads the encoded `requires`, never the stack. --- cli/src/castle_cli/commands/create.py | 16 +++++++++++++++- cli/src/castle_cli/manifest.py | 1 + cli/tests/test_create.py | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cli/src/castle_cli/commands/create.py b/cli/src/castle_cli/commands/create.py index ff3af6a..31fad48 100644 --- a/cli/src/castle_cli/commands/create.py +++ b/cli/src/castle_cli/commands/create.py @@ -16,6 +16,7 @@ from castle_cli.manifest import ( ManageSpec, PathDeployment, ProgramSpec, + Requirement, RunPython, SystemdDeployment, SystemdSpec, @@ -38,6 +39,14 @@ STACK_BUILD_OUTPUTS: dict[str, str] = { "react-vite": "dist", } +# Substrate a stack's apps depend on — seeded as a `requires` at creation so the +# relationship graph shows it. This keeps `stack` uncoupled from the runtime model: +# the stack declares the edge once here; the graph only ever reads the encoded +# `requires`, never the stack. See docs/relationships.md. +STACK_REQUIRES: dict[str, list[Requirement]] = { + "supabase": [Requirement(kind="deployment", ref="supabase")], +} + def next_available_port(config: object) -> int: """Find the next available port starting from 9001 (9000 is reserved for gateway).""" @@ -111,6 +120,8 @@ def run_create(args: argparse.Namespace) -> int: source=str(project_dir), stack=stack, build=build, + # Seed the stack's substrate dependency (e.g. supabase) as a real `requires`. + requires=list(STACK_REQUIRES.get(stack or "", [])), ) if kind == "tool": # A PATH-managed deployment: installed via `uv tool install`, no unit/route. @@ -120,7 +131,10 @@ def run_create(args: argparse.Namespace) -> int: elif kind == "static": # A caddy-managed static deployment: no systemd unit, served from the build dir. config.deployments[name] = CaddyDeployment( - id=name, manager="caddy", program=name, root=static_root or "dist", + id=name, + manager="caddy", + program=name, + root=static_root or "dist", description=description, ) elif kind == "service": diff --git a/cli/src/castle_cli/manifest.py b/cli/src/castle_cli/manifest.py index 2aae617..3b285e0 100644 --- a/cli/src/castle_cli/manifest.py +++ b/cli/src/castle_cli/manifest.py @@ -21,6 +21,7 @@ from castle_core.manifest import ( # noqa: F401 — explicit re-exports for typ Reach, ReadinessHttpGet, RemoteDeployment, + Requirement, RestartPolicy, RunCommand, RunCompose, diff --git a/cli/tests/test_create.py b/cli/tests/test_create.py index e4c0174..8acff36 100644 --- a/cli/tests/test_create.py +++ b/cli/tests/test_create.py @@ -107,6 +107,9 @@ class TestCreateCommand: comp = config.programs["guestbook"] assert comp.stack == "supabase" assert comp.build is not None and comp.build.outputs == ["public"] + # The supabase stack seeds a `requires` on the substrate so the graph shows + # the dependency (stack stays uncoupled — it just declares the edge once). + assert [(r.kind, r.ref) for r in comp.requires] == [("deployment", "supabase")] dep = config.deployments["guestbook"] assert dep.manager == "caddy" assert dep.root == "public"