lib/openagents/collective/review_receipt.ex

main at 58e6347eeb72 · 3 KB

defmodule OpenAgents.Collective.ReviewReceipt do
  @moduledoc "Append-only independent evaluation receipt for one generalized candidate."

  use Ecto.Schema
  import Ecto.Changeset

  @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, updated_at: false]

  schema "collective_review_receipts" do
    belongs_to :candidate, OpenAgents.Collective.Candidate
    belongs_to :generalization_receipt, OpenAgents.Collective.GeneralizationReceipt
    field :decision, :string
    field :reason_codes, {:array, :string}, default: []
    field :evaluator_artifact_ref, :string
    field :evaluator_artifact_digest, :string
    field :dataset_ref, :string
    field :dataset_digest, :string
    field :policy_id, :string
    field :policy_version, :integer
    field :policy_digest, :string
    field :candidate_digest, :string
    field :evaluation_digest, :string
    field :dimensions, :map
    field :reviewer_actor_id, :string
    field :reviewer_auth_method, :string
    timestamps()
  end

  @type t :: %__MODULE__{}

  def changeset(receipt, attributes) do
    receipt
    |> cast(attributes, [
      :candidate_id,
      :generalization_receipt_id,
      :decision,
      :reason_codes,
      :evaluator_artifact_ref,
      :evaluator_artifact_digest,
      :dataset_ref,
      :dataset_digest,
      :policy_id,
      :policy_version,
      :policy_digest,
      :candidate_digest,
      :evaluation_digest,
      :dimensions,
      :reviewer_actor_id,
      :reviewer_auth_method
    ])
    |> validate_required([
      :candidate_id,
      :generalization_receipt_id,
      :decision,
      :reason_codes,
      :evaluator_artifact_ref,
      :evaluator_artifact_digest,
      :dataset_ref,
      :dataset_digest,
      :policy_id,
      :policy_version,
      :policy_digest,
      :candidate_digest,
      :evaluation_digest,
      :dimensions,
      :reviewer_actor_id,
      :reviewer_auth_method
    ])
    |> validate_inclusion(:decision, ~w(passed rejected))
    |> validate_number(:policy_version, greater_than: 0)
    |> validate_format(:evaluator_artifact_digest, @digest_regex)
    |> validate_format(:dataset_digest, @digest_regex)
    |> validate_format(:policy_digest, @digest_regex)
    |> validate_format(:candidate_digest, @digest_regex)
    |> validate_format(:evaluation_digest, @digest_regex)
    |> validate_length(:evaluator_artifact_ref, min: 1, max: 256)
    |> validate_length(:dataset_ref, min: 1, max: 256)
    |> validate_length(:policy_id, min: 1, max: 128)
    |> validate_length(:reviewer_actor_id, min: 1, max: 256)
    |> validate_length(:reviewer_auth_method, min: 1, max: 128)
    |> validate_reason_codes()
    |> foreign_key_constraint(:candidate_id)
    |> foreign_key_constraint(:generalization_receipt_id)
    |> unique_constraint(:candidate_id)
  end

  defp validate_reason_codes(changeset) do
    validate_change(changeset, :reason_codes, fn :reason_codes, codes ->
      if is_list(codes) and length(codes) <= 16 and
           Enum.all?(codes, &(is_binary(&1) and byte_size(&1) in 1..64)),
         do: [],
         else: [reason_codes: "must contain bounded reason codes"]
    end)
  end
end