lib/openagents/forge.ex

main at 58e6347eeb72 · 3 KB

defmodule OpenAgents.Forge do
  @moduledoc """
  Sarah's own git forge — the public API of the bounded context (audit A6).

  This app serves the forge at `openagents.com/<owner>/<repo>.git` via stock Git
  smart-HTTP (`OpenAgents.Forge.GitHTTP`), with the WAL in object storage as ref
  truth (`OpenAgents.Forge.WAL`, Continuity-shaped per audit A7), bare repos on
  the stateful partition as per-node cache (`OpenAgents.Forge.Repos` /
  `OpenAgents.Forge.Sync`), pushes acked only after WAL persist
  (`OpenAgents.Forge.Pushes`), and Postgres holding derived receipts only.

  Hard edges: no imports from conversation contexts; emits receipts and
  PubSub (`forge:pushes`), never reads turns/memory/voice.
  """

  import Ecto.Query

  alias OpenAgents.Forge.{Pushes, PushReceipt, ReceiptRepository}
  alias OpenAgents.Repo

  @doc "Whether the forge endpoint is enabled (default true; env-gated in prod)."
  def enabled? do
    Application.get_env(:openagents, :forge_enabled, true)
  end

  @doc "Recent push receipts for one repo, newest first, bounded."
  def recent_pushes(repo, limit \\ 20) do
    repo_keys = Pushes.receipt_repo_keys(repo)

    PushReceipt
    |> where([p], p.repo in ^repo_keys)
    |> order_by([p], desc: p.wal_seq)
    |> limit(^limit)
    |> Repo.all()
  end

  @doc """
  Recent deploy receipts for one repo, newest first, bounded.

  A receipt that names its repository is matched by that key; `repo` is read
  only for a row the #181 backfill could not settle.
  """
  def recent_deploys(repo, limit \\ 20) do
    OpenAgents.Forge.DeployReceipt
    |> ReceiptRepository.scope(ReceiptRepository.resolve(repo), [repo])
    |> order_by([d], desc: d.inserted_at)
    |> limit(^limit)
    |> Repo.all()
  end

  @doc """
  Recent build receipts for one repo, newest first, bounded.

  Keyed the same way `recent_deploys/2` is.
  """
  def recent_builds(repo, limit \\ 20) do
    OpenAgents.Forge.BuildReceipt
    |> ReceiptRepository.scope(ReceiptRepository.resolve(repo), [repo])
    |> order_by([b], desc: b.inserted_at)
    |> limit(^limit)
    |> Repo.all()
  end

  @doc """
  Every receipt in the chain that references `sha` (full or abbreviated,
  ≥7 chars): the commit's deploy story for the public commit view (#137).
  Bounded scans; a sha older than the scan window honestly returns empty.
  """
  def receipts_for(repo, sha) do
    %{
      pushes:
        repo
        |> recent_pushes(200)
        |> Enum.filter(fn push ->
          push.refs
          |> Map.values()
          |> Enum.any?(fn
            %{"new" => new} -> sha_match?(sha, new)
            new when is_binary(new) -> sha_match?(sha, new)
            _ -> false
          end)
        end),
      builds: repo |> recent_builds(200) |> Enum.filter(&sha_match?(sha, &1.sha)),
      targets:
        repo |> OpenAgents.Forge.Targets.recent(100) |> Enum.filter(&sha_match?(sha, &1.sha)),
      deploys: repo |> recent_deploys(200) |> Enum.filter(&sha_match?(sha, &1.sha))
    }
  end

  def gate_receipts_for(repo, sha, opts \\ []) when is_binary(repo) and is_binary(sha) do
    git_common_dir = Keyword.get(opts, :git_common_dir, OpenAgents.Forge.Repos.bare_path(repo))

    case OpenAgents.Forge.GateReceipt.verify(sha, git_common_dir: git_common_dir) do
      {:ok, receipt} -> %{gates: [receipt]}
      _ -> %{gates: []}
    end
  end

  @doc "Prefix-match two shas in either direction (short vs full), ≥7 chars."
  def sha_match?(a, b) when is_binary(a) and is_binary(b) do
    min(byte_size(a), byte_size(b)) >= 7 and
      (String.starts_with?(a, b) or String.starts_with?(b, a))
  end

  def sha_match?(_a, _b), do: false

  @doc "The clone URL for a repo on this deployment."
  def clone_url(repo) do
    OpenAgentsWeb.Endpoint.url() <> "/OpenAgentsInc/#{repo}.git"
  end
end