priv/repo/migrations/20260824032138_create_issue_completion_claims.exs

main at 58e6347eeb72 · 4 KB

defmodule OpenAgents.Repo.Migrations.CreateIssueCompletionClaims do
  use Ecto.Migration

  def change do
    # The opt-in. Both flags default to false, so a repository that has said
    # nothing is a repository where nothing closes itself and no claim is even
    # graded. Silence is never consent here: the absent row and the row with
    # both flags false must mean the same thing.
    create table(:repository_closure_policies, primary_key: false) do
      add :id, :binary_id, primary_key: true

      add :repository_id, references(:repositories, type: :binary_id, on_delete: :delete_all),
        null: false

      # Whether agent-authored claims are graded at all. False makes every
      # claim `not_applicable`, which is the accepted-outcome contract's own
      # word for work outside the gate.
      add :agents_enabled, :boolean, null: false, default: false

      # Whether an accepted claim may close the issue. False records the
      # verdict and leaves the issue open for a person to act on.
      add :verified_closing_enabled, :boolean, null: false, default: false

      add :updated_by_user_id, references(:users, type: :binary_id, on_delete: :nilify_all)

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:repository_closure_policies, [:repository_id])

    create table(:issue_completion_claims, primary_key: false) do
      add :id, :binary_id, primary_key: true

      add :repository_id, references(:repositories, type: :binary_id, on_delete: :delete_all),
        null: false

      add :issue_id, references(:issues, on_delete: :delete_all), null: false

      # The attempt. Not nullable: three of the five attempt fields the
      # contract binds — the authority, the budget, and the revision — are
      # only reachable through an assignment, so a claim without one could
      # never be graded as anything but incomplete.
      add :assignment_id,
          references(:forge_assignments, type: :binary_id, on_delete: :delete_all), null: false

      # The exact revision the claim is about, copied from the attempt's own
      # terminal report rather than from the caller.
      add :revision, :string, null: false, size: 64

      # The graded verdict: the accepted-outcome contract's own vocabulary.
      add :state, :string, null: false, size: 32

      # The typed reasons a non-accepted verdict carries, rendered as strings.
      add :reasons, {:array, :string}, null: false, default: []

      # Which evidence satisfied which acceptance criterion, and nothing else.
      add :criteria, {:array, :map}, null: false, default: []

      # The verifier's identity and the falsifier it recorded, so a reader can
      # ask what observation would have made this red.
      add :verifier, :string, size: 200
      add :falsifier, :string, size: 500

      # The closure half. `closed_by_actor` is a system principal and never a
      # user: a person's close is a `issue_closing_references` row with a
      # `closed_by_user_id`, and the two records cannot be confused.
      add :closed, :boolean, null: false, default: false
      add :closed_at, :utc_datetime_usec
      add :closed_by_actor, :string, size: 200

      # A later receipt that disagreed with the evidence this claim rested on.
      # It never reopens the issue; it records that the ground moved.
      add :contradicted_at, :utc_datetime_usec

      add :contradicted_by_evidence_id,
          references(:issue_evidence, type: :binary_id, on_delete: :nilify_all)

      add :contradiction_reason, :string, size: 200

      timestamps(type: :utc_datetime_usec)
    end

    # `{issue, attempt, revision}` is the key #150 names. One attempt reporting
    # one revision produces one graded claim however many times it is
    # submitted, so a resubmission updates a verdict rather than accumulating
    # verdicts nobody can order.
    create unique_index(:issue_completion_claims, [:issue_id, :assignment_id, :revision])

    # The contradiction lookup: a failing receipt lands for a commit and asks
    # which claims rested on that exact revision of that exact issue.
    create index(:issue_completion_claims, [:issue_id, :revision])

    create constraint(:issue_completion_claims, :issue_completion_claims_state,
             check: "state IN ('accepted','incomplete','unauthorized','failed','not_applicable')"
           )

    create constraint(:issue_completion_claims, :issue_completion_claims_revision,
             check: "revision ~ '^[0-9a-f]{7,64}$'"
           )

    # Only an accepted claim may carry a close. A row that says it closed an
    # issue on an `incomplete` verdict is the exact failure this issue exists
    # to prevent, so PostgreSQL refuses it rather than trusting the caller.
    create constraint(:issue_completion_claims, :issue_completion_claims_close_requires_accepted,
             check: "closed = false OR (state = 'accepted' AND closed_at IS NOT NULL)"
           )
  end
end