Current section

Files

Jump to
agent_session_manager lib asm extensions workspace.ex
Raw

lib/asm/extensions/workspace.ex

defmodule ASM.Extensions.Workspace do
@moduledoc """
Public workspace extension API.
This domain provides workspace snapshot/diff/rollback primitives for
host-managed run orchestration without coupling core runtime internals.
"""
use Boundary,
deps: [ASM],
exports: [
Backend,
Diff,
GitBackend,
HashBackend,
Snapshot
]
alias ASM.Error
alias ASM.Extensions.Workspace.{Diff, GitBackend, HashBackend, Snapshot}
@typedoc "Configured backend selection strategy."
@type backend_kind :: :auto | :git | :hash
@typedoc "Workspace snapshot operation result."
@type snapshot_result :: {:ok, Snapshot.t()} | {:error, Error.t()}
@typedoc "Workspace diff operation result."
@type diff_result :: {:ok, Diff.t()} | {:error, Error.t()}
@typedoc "Workspace rollback operation result."
@type rollback_result :: :ok | {:error, Error.t()}
@spec snapshot(String.t(), keyword()) :: snapshot_result()
def snapshot(root, opts \\ []) when is_binary(root) and is_list(opts) do
backend_kind = Keyword.get(opts, :backend, :auto)
with {:ok, normalized_root} <- normalize_root(root),
{:ok, backend_module} <- resolve_backend(backend_kind, normalized_root),
{:ok, %Snapshot{} = snapshot} <-
safe_backend_call(
fn -> backend_module.snapshot(normalized_root, opts) end,
"snapshot/2 failed"
) do
{:ok, snapshot}
end
end
@spec diff(Snapshot.t(), Snapshot.t(), keyword()) :: diff_result()
def diff(%Snapshot{} = from_snapshot, %Snapshot{} = to_snapshot, opts \\ [])
when is_list(opts) do
with :ok <- validate_diff_inputs(from_snapshot, to_snapshot),
{:ok, backend_module} <- backend_module(from_snapshot.backend),
{:ok, %Diff{} = diff} <-
safe_backend_call(
fn -> backend_module.diff(from_snapshot, to_snapshot, opts) end,
"diff/3 failed"
) do
{:ok, diff}
end
end
@spec rollback(Snapshot.t(), keyword()) :: rollback_result()
def rollback(%Snapshot{} = snapshot, opts \\ []) when is_list(opts) do
case backend_module(snapshot.backend) do
{:ok, backend_module} ->
safe_backend_call(
fn -> backend_module.rollback(snapshot, opts) end,
"rollback/2 failed"
)
{:error, %Error{} = error} ->
{:error, error}
end
end
defp normalize_root(root) do
expanded = Path.expand(root)
if File.dir?(expanded) do
{:ok, expanded}
else
{:error,
Error.new(
:config_invalid,
:config,
"workspace root does not exist or is not a directory: #{inspect(root)}"
)}
end
end
defp resolve_backend(:auto, root) do
if GitBackend.available?(root), do: {:ok, GitBackend}, else: {:ok, HashBackend}
end
defp resolve_backend(:git, root) do
if GitBackend.available?(root) do
{:ok, GitBackend}
else
{:error,
Error.new(
:config_invalid,
:config,
"git backend requires a git repository root and a working git executable"
)}
end
end
defp resolve_backend(:hash, _root), do: {:ok, HashBackend}
defp resolve_backend(other, _root) do
{:error,
Error.new(
:config_invalid,
:config,
"unsupported workspace backend: #{inspect(other)}"
)}
end
defp backend_module(:git), do: {:ok, GitBackend}
defp backend_module(:hash), do: {:ok, HashBackend}
defp backend_module(other) do
{:error,
Error.new(
:config_invalid,
:config,
"unsupported workspace snapshot backend: #{inspect(other)}"
)}
end
defp validate_diff_inputs(%Snapshot{} = from_snapshot, %Snapshot{} = to_snapshot) do
cond do
from_snapshot.backend != to_snapshot.backend ->
{:error,
Error.new(
:config_invalid,
:config,
"workspace diff requires snapshots from the same backend"
)}
not same_path?(from_snapshot.root, to_snapshot.root) ->
{:error,
Error.new(
:config_invalid,
:config,
"workspace diff requires snapshots from the same root"
)}
true ->
:ok
end
end
defp safe_backend_call(fun, operation) when is_function(fun, 0) and is_binary(operation) do
case fun.() do
:ok ->
:ok
{:ok, value} ->
{:ok, value}
{:error, %Error{} = error} ->
{:error, error}
other ->
{:error, Error.new(:unknown, :runtime, operation, cause: other)}
end
rescue
error in [Error] ->
{:error, error}
error ->
{:error, Error.new(:unknown, :runtime, operation, cause: error)}
catch
:exit, reason ->
{:error, Error.new(:unknown, :runtime, operation, cause: reason)}
end
defp same_path?(left, right) do
Path.expand(left) == Path.expand(right)
end
end