lib/openagents/continual_learning/receipt.ex

58e6347eeb72 · 1 KB

defmodule OpenAgents.ContinualLearning.Receipt do
  @moduledoc """
  One append-only receipt of a continual-learning job.

  The sequence is dense per job, so a reader can tell a missing receipt from a
  receipt that was never written, and every payload carries its own canonical
  digest.
  """

  use Ecto.Schema
  import Ecto.Changeset

  @kinds ~w(admission usage energy training evaluation artifact settlement resume refusal)
  @digest_regex ~r/\A[0-9a-f]{64}\z/

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]

  schema "continual_learning_receipts" do
    belongs_to :job, OpenAgents.ContinualLearning.Job
    field :kind, :string
    field :sequence, :integer
    field :receipt_ref, :string
    field :payload, :map, default: %{}
    field :digest, :string

    timestamps()
  end

  @type t :: %__MODULE__{}

  def kinds, do: @kinds

  def changeset(receipt, attributes) do
    receipt
    |> cast(attributes, [:kind, :sequence, :receipt_ref, :payload, :digest])
    |> validate_required([:kind, :sequence, :receipt_ref, :payload, :digest])
    |> validate_inclusion(:kind, @kinds)
    |> validate_number(:sequence, greater_than: 0)
    |> validate_length(:receipt_ref, min: 1, max: 256)
    |> validate_format(:digest, @digest_regex)
    |> unique_constraint([:job_id, :sequence])
    |> unique_constraint(:receipt_ref)
    |> foreign_key_constraint(:job_id)
  end
end