Define the accepted-outcome contract for agent-authored work

2fd7e1edb2b0 · Devin AI · · parent 8adfd9b1d242

Define the accepted-outcome contract for agent-authored work

An agent-authored completion claim now counts as an accepted outcome only
when a scoped issue, a bound execution attempt, an admitted verifier, a
recorded falsifier, a terminal result, and per-criterion evidence all hold.
Anything less produces a typed non-accepted result, and human-only work and
agents-disabled repositories stay outside the gate.

- Publish priv/api-contracts/accepted-outcome-v1.json and evaluate claims
  in OpenAgents.AcceptedOutcome with a privacy-safe public projection.
- Name the five episode-252 false-green classes in the contract and fail
  any green result that carries one.
- Align the implementation issue template and the definition of done in
  docs with the same contract, and record OUTCOME-001 in INVARIANTS.md.

Closes #66.

Co-Authored-By: Christopher David <chris@openagents.com>
Co-Authored-By
Christopher David <chris@openagents.com>
Closes
#66

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified INVARIANTS.md
  • modified docs/2026-08-21-issues-projects-work-system-assessment.md
  • added docs/accepted-outcome-contract.md
  • added lib/openagents/accepted_outcome.ex
  • added priv/api-contracts/accepted-outcome-v1.json
  • added test/openagents/accepted_outcome_test.exs

Diff

6 files changed, +650 -5

INVARIANTS.md modified +45

@@ -1154,6 +1154,50 @@ Evidence: `OpenAgents.SCV.Deployments`, `OpenAgents.Work.Scv`,

1154 1154
(the `scv` kind), `OpenAgents.RuntimeConfig`, and
1155 1155
`test/openagents/scv/deployments_test.exs`.
1156 1156
1157
### OUTCOME-001 — An agent-authored claim is accepted only against the accepted-outcome contract
1158
1159
Status: Current
1160
1161
When an agent claims that work is complete, the claim counts as an accepted
1162
outcome only when every part the contract names holds; anything less is a
1163
typed non-accepted result, and human-only work stays outside the gate
1164
entirely.
1165
1166
- **The issue is the canonical record, and it must be scoped.** A claim
1167
  anchors to an issue that states its problem, scope, acceptance criteria,
1168
  and success metrics. A claim against an issue missing any of those sections
1169
  is `incomplete`, never accepted.
1170
- **The attempt is bound, not implied.** Each execution attempt records the
1171
  issue number, repository, authority, budget, and exact revision it
1172
  produced. An attempt bound to a different issue or repository, an
1173
  unadmitted verifier, or a violated producer-verifier separation policy is
1174
  `unauthorized`.
1175
- **Green must have been able to be red.** The claim records an admitted
1176
  verifier, a falsifier, and a terminal result. A failed terminal result is
1177
  `failed`, and a result carrying any of the five named false-green classes
1178
  — `false_green_fixture_assert`, `false_green_api_mirror`,
1179
  `false_green_mocked_seam`, `false_green_coverage_theater`,
1180
  `false_green_round_up` — is `failed` even when the verifier reported green.
1181
- **Every criterion names its evidence.** An accepted outcome explains which
1182
  receipt satisfied each acceptance criterion, so the issue page can show the
1183
  mapping; a criterion with no evidence makes the claim `incomplete`.
1184
- **The public projection is content-free about private material.** A
1185
  projection of an evaluation carries the result state, typed reasons,
1186
  criterion names, and public receipt references only — never prompts, logs,
1187
  private repository names, or private receipt references.
1188
- **Human-only work is not gated.** Work by a human actor and repositories
1189
  with agents disabled evaluate to `not_applicable` and remain fully usable.
1190
1191
The committed contract is `priv/api-contracts/accepted-outcome-v1.json`, and
1192
`OpenAgents.AcceptedOutcome.validate/1` refuses a contract whose required
1193
sections, attempt fields, false-green classes, or result states drift from
1194
the code that enforces them.
1195
1196
Evidence: `OpenAgents.AcceptedOutcome`,
1197
`priv/api-contracts/accepted-outcome-v1.json`,
1198
`docs/accepted-outcome-contract.md`, and
1199
`test/openagents/accepted_outcome_test.exs`.
1200
1157 1201
## Interface and release
1158 1202
1159 1203
### VOICE-001 — Spoken identity is admitted before media

@@ -2081,6 +2125,7 @@ contract; the invariant prose above defines the assertion, not the filename.

2081 2125
| WORK-001 | `test/openagents/work_job_test.exs`, `test/openagents/deep_work_tool_loop_test.exs` |
2082 2126
| SELF-EDIT-001 | `test/openagents/tools/repository_mutation_tools_test.exs`, `test/openagents/coding_job_test.exs` |
2083 2127
| SCV-001 | `test/openagents/scv/deployments_test.exs` |
2128
| OUTCOME-001 | `test/openagents/accepted_outcome_test.exs` |
2084 2129
| VOICE-001 | `test/openagents/voice/config_test.exs` |
2085 2130
| VOICE-002 | `test/openagents_web/controllers/voice_call_controller_test.exs` |
2086 2131
| VOICE-003 | `test/openagents/voice_test.exs`, `test/openagents/voice_sessions_test.exs` |
docs/2026-08-21-issues-projects-work-system-assessment.md modified +21 -5

@@ -477,16 +477,22 @@ that qualified together.

477 477
478 478
## Issue template for this program
479 479
480
Every implementation issue should contain these sections:
480
Every implementation issue must contain the four sections the
481
accepted-outcome contract requires — problem, scope, acceptance criteria, and
482
success metrics — plus the supporting sections below. See
483
`accepted-outcome-contract.md` for how these sections gate agent-authored
484
completion claims.
481 485
482 486
```md
483
## Outcome
487
## Problem
484 488
485
Describe what a person or agent can do after this ships.
489
Describe the measured behavior that motivates the work and link to code,
490
tests, screenshots, or logs.
486 491
487
## Current behavior
492
## Scope
488 493
489
Describe the measured behavior and link to code, tests, screenshots, or logs.
494
State what this issue changes and what it leaves alone. Name the outcome a
495
person or agent can reach after this ships.
490 496
491 497
## Contract
492 498

@@ -498,6 +504,10 @@ List the affected web routes, API routes, schemas, events, and authority rules.

498 504
- Include public and private repository behavior where relevant.
499 505
- Include unauthorized behavior and error results.
500 506
507
## Success metrics
508
509
State how you can measure that the outcome holds after it ships.
510
501 511
## Verification
502 512
503 513
List focused tests, `mix precommit`, CLI checks, browser checks, migration

@@ -515,6 +525,12 @@ release the CLI that consumes it.

515 525
516 526
## Definition of done
517 527
528
Agent-authored work uses the accepted-outcome contract in
529
`accepted-outcome-contract.md` as its definition of done: a completion claim
530
counts only when the scoped issue, bound attempt, admitted verifier,
531
recorded falsifier, terminal result, and per-criterion evidence all hold.
532
Contribution and review apply that same definition.
533
518 534
An issue is done only when all applicable conditions hold:
519 535
520 536
- The outcome and authority behavior have focused tests.
docs/accepted-outcome-contract.md added +100

@@ -0,0 +1,100 @@

1
# Accepted-outcome contract
2
3
This document defines when work that an agent claims is complete counts as an
4
accepted outcome. It is the shared definition of done for contribution and
5
review, and it is falsifiable: every part of a claim can be checked, and a
6
claim that cannot be checked is not accepted.
7
8
The published contract lives at `priv/api-contracts/accepted-outcome-v1.json`.
9
`OpenAgents.AcceptedOutcome` evaluates claims against it, and
10
`test/openagents/accepted_outcome_test.exs` proves the accepted, failed,
11
incomplete, unauthorized, and private-evidence cases. The issue stays the
12
canonical work record; the contract only grades claims about it.
13
14
## The parts of a claim
15
16
An accepted outcome connects five things. Remove any one of them and the
17
claim is not accepted.
18
19
1. **A scoped issue.** The implementation issue states the requested outcome
20
   in four required sections: problem, scope, acceptance criteria, and
21
   success metrics. An issue without them cannot anchor an accepted outcome.
22
2. **A bound attempt.** Each execution attempt records the issue number, the
23
   repository, the authority it acted under, its budget, and the exact
24
   revision it produced. An attempt bound to a different issue or repository
25
   is unauthorized.
26
3. **Independent verification.** Evidence counts only when the verifier is
27
   admitted. When policy requires producer-verifier separation, the verifier
28
   must also be independent of the producer.
29
4. **A falsifier and a terminal result.** The claim records what observation
30
   would have proven it wrong and whether the verifier passed or failed, so
31
   every green result could have been red.
32
5. **Inspectable evidence.** Each acceptance criterion names the receipt that
33
   satisfied it, and the issue page can explain which evidence satisfied
34
   which criterion.
35
36
## Typed non-accepted results
37
38
A claim that falls short produces a typed result, never silence:
39
40
- `incomplete` — the claim is structurally short: a missing issue section, a
41
  missing attempt field, a missing verifier, falsifier, or terminal result,
42
  or an acceptance criterion with no evidence.
43
- `unauthorized` — the attempt is not bound to the issue and repository it
44
  changed, the verifier is not admitted, or required producer-verifier
45
  separation does not hold.
46
- `failed` — the verifier reported failure, or a false-green class is
47
  recorded against the result.
48
49
## False-green classes
50
51
Review and receipt data name five failure modes in which a green result does
52
not prove the outcome:
53
54
- `false_green_fixture_assert` — the test asserts on its own fixture rather
55
  than the behavior.
56
- `false_green_api_mirror` — the test restates the implementation instead of
57
  observing an outcome.
58
- `false_green_mocked_seam` — the mocked boundary is the behavior under test.
59
- `false_green_coverage_theater` — coverage numbers stand in for assertions.
60
- `false_green_round_up` — a partial result is reported as complete.
61
62
A result that names any of these classes is `failed` even when the verifier
63
reported green.
64
65
## Visibility
66
67
Public projections of an evaluation carry only the result state, typed
68
reasons, criterion names, and receipt references that are public. They never
69
carry prompts, logs, private repository names, or private receipt references.
70
A criterion satisfied by private evidence appears publicly as satisfied by
71
private evidence, without the reference.
72
73
## Human-only work
74
75
The contract gates only agent-authored claims. Human-only work and
76
repositories with agents disabled evaluate to `not_applicable` and remain
77
fully usable; nothing about the issue workflow changes for them.
78
79
## Definition of done for agent work
80
81
An agent-authored change is done when its claim evaluates to `accepted`:
82
83
- The issue is scoped with problem, scope, acceptance criteria, and success
84
  metrics.
85
- The attempt binds the issue, repository, authority, budget, and exact
86
  revision.
87
- An admitted verifier — independent when policy requires separation —
88
  recorded a falsifier and passed.
89
- Every acceptance criterion names its inspectable receipt.
90
- No false-green class is recorded against the result.
91
92
Contribution and review use this same definition. Reviewers grade claims, not
93
prose: a report that sounds complete but fails a check above is a typed
94
non-accepted result, and the review states which one.
95
96
## Related records
97
98
- Issue-to-job and receipt linkage: `docs/2026-08-21-issues-projects-work-system-assessment.md`, Track E.
99
- Source episodes: `docs/episode-triage.md`, episodes 237, 251, 252, 259, and 264.
100
- Invariant: `INVARIANTS.md`, OUTCOME-001.
lib/openagents/accepted_outcome.ex added +249

@@ -0,0 +1,249 @@

1
defmodule OpenAgents.AcceptedOutcome do
2
  @moduledoc """
3
  Evaluates agent-authored completion claims against the accepted-outcome
4
  contract (`priv/api-contracts/accepted-outcome-v1.json`).
5
6
  The contract connects the requested outcome (a scoped issue), execution
7
  authority (a bound attempt), the exact change (a revision), independent
8
  verification (an admitted verifier with a recorded falsifier), and terminal
9
  evidence (per-criterion receipts). The issue stays the canonical work
10
  record; this module only grades a claim about it.
11
12
  A claim that cannot satisfy the contract produces a typed non-accepted
13
  result — `:incomplete`, `:unauthorized`, or `:failed` — with the exact
14
  reasons, so every green result could have been red. Human-only work and
15
  repositories with agents disabled are outside the contract and return
16
  `:not_applicable`.
17
  """
18
19
  @contract_path "priv/api-contracts/accepted-outcome-v1.json"
20
21
  @required_issue_sections ~w(problem scope acceptance_criteria success_metrics)a
22
  @required_attempt_fields ~w(issue_number repository authority budget revision)a
23
  @terminal_results ~w(passed failed)a
24
  @false_green_classes ~w(
25
    false_green_fixture_assert
26
    false_green_api_mirror
27
    false_green_mocked_seam
28
    false_green_coverage_theater
29
    false_green_round_up
30
  )
31
  @non_accepted_types ~w(incomplete unauthorized failed)a
32
  @exemptions ~w(human_only_work agents_disabled_repository)a
33
34
  @doc "The decoded and validated public contract."
35
  def load do
36
    with {:ok, bytes} <- File.read(contract_file()),
37
         {:ok, contract} <- Jason.decode(bytes),
38
         :ok <- validate(contract) do
39
      {:ok, contract}
40
    end
41
  end
42
43
  @doc "The issue-template sections a scoped implementation issue must carry."
44
  def required_issue_sections, do: @required_issue_sections
45
46
  @doc "The fields that bind one execution attempt to its authorized change."
47
  def required_attempt_fields, do: @required_attempt_fields
48
49
  @doc "The five named false-green classes from episode 252."
50
  def false_green_classes, do: @false_green_classes
51
52
  @doc "The typed non-accepted result types."
53
  def non_accepted_types, do: @non_accepted_types
54
55
  @doc "Validates the committed contract document."
56
  def validate(%{
57
        "contract" => "openagents.accepted-outcome.v1",
58
        "version" => 1,
59
        "issue_template" => %{"required_sections" => sections},
60
        "attempt_binding" => %{"required_fields" => fields},
61
        "verification" => %{"terminal_results" => terminal_results},
62
        "false_green_classes" => false_greens,
63
        "result_states" => %{
64
          "non_accepted" => non_accepted,
65
          "not_applicable" => not_applicable
66
        },
67
        "visibility" => %{"public_projection_excludes" => excludes}
68
      }) do
69
    committed = [
70
      {sections, Enum.map(@required_issue_sections, &Atom.to_string/1)},
71
      {fields, Enum.map(@required_attempt_fields, &Atom.to_string/1)},
72
      {terminal_results, Enum.map(@terminal_results, &Atom.to_string/1)},
73
      {false_greens, @false_green_classes},
74
      {non_accepted, Enum.map(@non_accepted_types, &Atom.to_string/1)},
75
      {not_applicable, Enum.map(@exemptions, &Atom.to_string/1)}
76
    ]
77
78
    if Enum.all?(committed, fn {document, code} -> document == code end) and
79
         is_list(excludes) and excludes != [] do
80
      :ok
81
    else
82
      {:error, :contract_divergence}
83
    end
84
  end
85
86
  def validate(_contract), do: {:error, :invalid_contract}
87
88
  @doc """
89
  Evaluates one completion claim.
90
91
  Returns one of:
92
93
    * `{:not_applicable, exemption}` — human-only work or a repository with
94
      agents disabled; the contract does not gate it.
95
    * `{:accepted, outcome}` — every acceptance criterion names its evidence,
96
      the attempt binds the exact revision, and an admitted verifier passed
97
      against a recorded falsifier. `outcome.criteria` explains which evidence
98
      satisfied each criterion.
99
    * `{:not_accepted, type, reasons}` — a typed refusal: `:incomplete` for a
100
      structurally incomplete claim, `:unauthorized` for a claim outside its
101
      granted authority or verifier admission, `:failed` for a failed verifier
102
      result or a named false-green class.
103
  """
104
  def evaluate(claim) when is_map(claim) do
105
    with :applicable <- applicability(claim),
106
         [] <- incomplete_reasons(claim),
107
         [] <- unauthorized_reasons(claim),
108
         [] <- failed_reasons(claim) do
109
      {:accepted, outcome(claim)}
110
    else
111
      {:not_applicable, exemption} -> {:not_applicable, exemption}
112
      [{type, _detail} | _rest] = reasons -> {:not_accepted, type, Keyword.values(reasons)}
113
    end
114
  end
115
116
  @doc """
117
  The bounded public projection of an evaluation result.
118
119
  The projection carries only result state, typed reasons, criterion names,
120
  and receipt references whose visibility is `:public`. It never carries
121
  prompts, logs, private repository names, or private receipt references, so
122
  a public issue page can explain the result without leaking evidence.
123
  """
124
  def public_projection({:not_applicable, exemption}) do
125
    %{state: :not_applicable, exemption: exemption}
126
  end
127
128
  def public_projection({:not_accepted, type, reasons}) do
129
    %{state: :not_accepted, type: type, reasons: reasons}
130
  end
131
132
  def public_projection({:accepted, outcome}) do
133
    %{
134
      state: :accepted,
135
      issue_number: outcome.issue_number,
136
      revision: outcome.revision,
137
      criteria:
138
        Enum.map(outcome.criteria, fn item ->
139
          case item.visibility do
140
            :public -> %{criterion: item.criterion, evidence: item.receipt}
141
            _restricted -> %{criterion: item.criterion, evidence: :private}
142
          end
143
        end)
144
    }
145
  end
146
147
  defp applicability(claim) do
148
    cond do
149
      claim[:actor] != :agent -> {:not_applicable, :human_only_work}
150
      claim[:agents_enabled] != true -> {:not_applicable, :agents_disabled_repository}
151
      true -> :applicable
152
    end
153
  end
154
155
  defp incomplete_reasons(claim) do
156
    issue = claim[:issue] || %{}
157
    attempt = claim[:attempt] || %{}
158
    verification = claim[:verification] || %{}
159
160
    missing_sections =
161
      for section <- @required_issue_sections,
162
          blank?(get_in(issue, [:sections, section])),
163
          do: {:incomplete, {:missing_issue_section, section}}
164
165
    missing_fields =
166
      for field <- @required_attempt_fields,
167
          blank?(attempt[field]),
168
          do: {:incomplete, {:missing_attempt_field, field}}
169
170
    missing_records =
171
      [
172
        {blank?(verification[:verifier]), :missing_verifier},
173
        {blank?(verification[:falsifier]), :missing_falsifier},
174
        {verification[:terminal_result] not in @terminal_results, :missing_terminal_result}
175
      ]
176
      |> Enum.filter(&elem(&1, 0))
177
      |> Enum.map(fn {_missing, reason} -> {:incomplete, reason} end)
178
179
    unevidenced =
180
      for criterion <- acceptance_criteria(issue),
181
          not Enum.any?(
182
            evidence(claim),
183
            &(&1[:criterion] == criterion and present?(&1[:receipt]))
184
          ),
185
          do: {:incomplete, {:unevidenced_criterion, criterion}}
186
187
    missing_sections ++ missing_fields ++ missing_records ++ unevidenced
188
  end
189
190
  defp unauthorized_reasons(claim) do
191
    issue = claim[:issue] || %{}
192
    attempt = claim[:attempt] || %{}
193
    verification = claim[:verification] || %{}
194
    verifier = verification[:verifier] || %{}
195
196
    [
197
      {attempt[:issue_number] != issue[:number] or attempt[:repository] != issue[:repository],
198
       :attempt_not_bound_to_issue},
199
      {verifier[:admitted] != true, :verifier_not_admitted},
200
      {verification[:separation_required] == true and
201
         verifier[:independent_of_producer] != true, :verifier_not_independent}
202
    ]
203
    |> Enum.filter(&elem(&1, 0))
204
    |> Enum.map(fn {_failed, reason} -> {:unauthorized, reason} end)
205
  end
206
207
  defp failed_reasons(claim) do
208
    verification = claim[:verification] || %{}
209
    named_classes = Enum.filter(List.wrap(verification[:false_green_classes]), &present?/1)
210
211
    [
212
      {verification[:terminal_result] == :failed, :verifier_failed},
213
      {named_classes != [], {:false_green, named_classes}}
214
    ]
215
    |> Enum.filter(&elem(&1, 0))
216
    |> Enum.map(fn {_failed, reason} -> {:failed, reason} end)
217
  end
218
219
  defp outcome(claim) do
220
    issue = claim[:issue]
221
    verification = claim[:verification]
222
223
    %{
224
      issue_number: issue[:number],
225
      repository: issue[:repository],
226
      revision: claim[:attempt][:revision],
227
      verifier: verification[:verifier][:id],
228
      falsifier: verification[:falsifier],
229
      criteria:
230
        for criterion <- acceptance_criteria(issue) do
231
          item = Enum.find(evidence(claim), &(&1[:criterion] == criterion))
232
          %{criterion: criterion, receipt: item[:receipt], visibility: item[:visibility]}
233
        end
234
    }
235
  end
236
237
  defp acceptance_criteria(issue), do: List.wrap(get_in(issue, [:sections, :acceptance_criteria]))
238
239
  defp evidence(claim), do: List.wrap(claim[:evidence])
240
241
  defp blank?(value), do: not present?(value)
242
243
  defp present?(value) when is_binary(value), do: String.trim(value) != ""
244
  defp present?(value) when is_map(value), do: map_size(value) > 0
245
  defp present?(value) when is_list(value), do: value != []
246
  defp present?(value), do: not is_nil(value)
247
248
  defp contract_file, do: Application.app_dir(:openagents, @contract_path)
249
end
priv/api-contracts/accepted-outcome-v1.json added +63

@@ -0,0 +1,63 @@

1
{
2
  "contract": "openagents.accepted-outcome.v1",
3
  "version": 1,
4
  "published_at": "2026-08-23",
5
  "summary": "When an agent claims that work is complete, the claim counts as an accepted outcome only when a scoped issue, a bound execution attempt, an admitted verifier result, a recorded falsifier, and inspectable evidence all hold. Anything less produces a typed non-accepted result. The issue remains the canonical work record.",
6
  "issue_template": {
7
    "required_sections": [
8
      "problem",
9
      "scope",
10
      "acceptance_criteria",
11
      "success_metrics"
12
    ]
13
  },
14
  "attempt_binding": {
15
    "required_fields": [
16
      "issue_number",
17
      "repository",
18
      "authority",
19
      "budget",
20
      "revision"
21
    ]
22
  },
23
  "verification": {
24
    "required_records": [
25
      "verifier",
26
      "falsifier",
27
      "terminal_result"
28
    ],
29
    "terminal_results": [
30
      "passed",
31
      "failed"
32
    ],
33
    "verifier_admission": "Evidence counts only when the verifier is admitted.",
34
    "producer_verifier_separation": "When policy requires producer-verifier separation, the verifier must be independent of the producer."
35
  },
36
  "false_green_classes": [
37
    "false_green_fixture_assert",
38
    "false_green_api_mirror",
39
    "false_green_mocked_seam",
40
    "false_green_coverage_theater",
41
    "false_green_round_up"
42
  ],
43
  "result_states": {
44
    "accepted": "Every acceptance criterion names the evidence that satisfied it, the verifier passed against a recorded falsifier, and the attempt binds the exact revision.",
45
    "non_accepted": [
46
      "incomplete",
47
      "unauthorized",
48
      "failed"
49
    ],
50
    "not_applicable": [
51
      "human_only_work",
52
      "agents_disabled_repository"
53
    ]
54
  },
55
  "visibility": {
56
    "public_projection_excludes": [
57
      "prompts",
58
      "logs",
59
      "private_repositories",
60
      "private_receipts"
61
    ]
62
  }
63
}
test/openagents/accepted_outcome_test.exs added +172

@@ -0,0 +1,172 @@

1
defmodule OpenAgents.AcceptedOutcomeTest do
2
  use ExUnit.Case, async: true
3
4
  alias OpenAgents.AcceptedOutcome
5
6
  @criteria [
7
    "The command exits zero on the fixture repository.",
8
    "An unauthorized caller receives a typed refusal."
9
  ]
10
11
  defp claim(overrides \\ %{}) do
12
    Map.merge(
13
      %{
14
        actor: :agent,
15
        agents_enabled: true,
16
        issue: %{
17
          number: 66,
18
          repository: "OpenAgentsInc/openagents.com",
19
          sections: %{
20
            problem: "Agent reports can sound complete without proof.",
21
            scope: "Grade completion claims; keep the issue canonical.",
22
            acceptance_criteria: @criteria,
23
            success_metrics: ["Zero false-completion incidents."]
24
          }
25
        },
26
        attempt: %{
27
          issue_number: 66,
28
          repository: "OpenAgentsInc/openagents.com",
29
          authority: %{token_scope: "forge:write"},
30
          budget: %{tool_calls: 32},
31
          revision: "8c4f2b1a9d3e"
32
        },
33
        verification: %{
34
          verifier: %{id: "ci-gate", admitted: true, independent_of_producer: true},
35
          separation_required: true,
36
          falsifier: "The fixture run fails when the fix is reverted.",
37
          terminal_result: :passed,
38
          false_green_classes: []
39
        },
40
        evidence: [
41
          %{criterion: Enum.at(@criteria, 0), receipt: "receipt:gate:41", visibility: :public},
42
          %{criterion: Enum.at(@criteria, 1), receipt: "receipt:test:87", visibility: :private}
43
        ]
44
      },
45
      overrides
46
    )
47
  end
48
49
  test "the committed contract is valid and matches the code" do
50
    assert {:ok, contract} = AcceptedOutcome.load()
51
52
    assert contract["false_green_classes"] == AcceptedOutcome.false_green_classes()
53
54
    divergent = put_in(contract, ["result_states", "non_accepted"], ["failed"])
55
    assert {:error, :contract_divergence} = AcceptedOutcome.validate(divergent)
56
57
    assert {:error, :invalid_contract} = AcceptedOutcome.validate(%{"contract" => "other"})
58
  end
59
60
  test "a complete, bound, verified claim is accepted and explains each criterion" do
61
    assert {:accepted, outcome} = AcceptedOutcome.evaluate(claim())
62
63
    assert outcome.issue_number == 66
64
    assert outcome.revision == "8c4f2b1a9d3e"
65
    assert outcome.verifier == "ci-gate"
66
    assert outcome.falsifier =~ "reverted"
67
68
    assert Enum.map(outcome.criteria, & &1.criterion) == @criteria
69
    assert Enum.all?(outcome.criteria, &(&1.receipt != nil))
70
  end
71
72
  test "a failed verifier result produces a typed failed result" do
73
    failed = claim(%{verification: %{claim().verification | terminal_result: :failed}})
74
75
    assert {:not_accepted, :failed, [:verifier_failed]} = AcceptedOutcome.evaluate(failed)
76
  end
77
78
  test "a named false-green class fails even when the verifier reported green" do
79
    verification = %{claim().verification | false_green_classes: ["false_green_mocked_seam"]}
80
81
    assert {:not_accepted, :failed, [{:false_green, ["false_green_mocked_seam"]}]} =
82
             AcceptedOutcome.evaluate(claim(%{verification: verification}))
83
  end
84
85
  test "a structurally incomplete claim produces a typed incomplete result" do
86
    base = claim()
87
88
    unscoped =
89
      claim(%{issue: put_in(base.issue, [:sections, :success_metrics], [])})
90
91
    assert {:not_accepted, :incomplete, [{:missing_issue_section, :success_metrics}]} =
92
             AcceptedOutcome.evaluate(unscoped)
93
94
    no_revision = claim(%{attempt: %{base.attempt | revision: nil}})
95
96
    assert {:not_accepted, :incomplete, [{:missing_attempt_field, :revision}]} =
97
             AcceptedOutcome.evaluate(no_revision)
98
99
    no_falsifier = claim(%{verification: %{base.verification | falsifier: "  "}})
100
101
    assert {:not_accepted, :incomplete, [:missing_falsifier]} =
102
             AcceptedOutcome.evaluate(no_falsifier)
103
104
    unevidenced = claim(%{evidence: [hd(base.evidence)]})
105
106
    assert {:not_accepted, :incomplete, [{:unevidenced_criterion, criterion}]} =
107
             AcceptedOutcome.evaluate(unevidenced)
108
109
    assert criterion == Enum.at(@criteria, 1)
110
  end
111
112
  test "an unbound or unadmitted attempt produces a typed unauthorized result" do
113
    base = claim()
114
115
    unbound = claim(%{attempt: %{base.attempt | issue_number: 67}})
116
117
    assert {:not_accepted, :unauthorized, [:attempt_not_bound_to_issue]} =
118
             AcceptedOutcome.evaluate(unbound)
119
120
    verifier = %{base.verification.verifier | admitted: false}
121
122
    assert {:not_accepted, :unauthorized, [:verifier_not_admitted]} =
123
             AcceptedOutcome.evaluate(
124
               claim(%{verification: %{base.verification | verifier: verifier}})
125
             )
126
127
    dependent = %{base.verification.verifier | independent_of_producer: false}
128
129
    assert {:not_accepted, :unauthorized, [:verifier_not_independent]} =
130
             AcceptedOutcome.evaluate(
131
               claim(%{verification: %{base.verification | verifier: dependent}})
132
             )
133
  end
134
135
  test "producer-verifier separation is required only when policy requires it" do
136
    base = claim()
137
    dependent = %{base.verification.verifier | independent_of_producer: false}
138
139
    verification = %{base.verification | verifier: dependent, separation_required: false}
140
141
    assert {:accepted, _outcome} = AcceptedOutcome.evaluate(claim(%{verification: verification}))
142
  end
143
144
  test "the public projection never carries private evidence" do
145
    projection = AcceptedOutcome.public_projection(AcceptedOutcome.evaluate(claim()))
146
147
    assert projection.state == :accepted
148
149
    assert [
150
             %{evidence: "receipt:gate:41"},
151
             %{evidence: :private}
152
           ] = projection.criteria
153
154
    refute inspect(projection) =~ "receipt:test:87"
155
156
    refusal =
157
      AcceptedOutcome.evaluate(
158
        claim(%{verification: %{claim().verification | terminal_result: :failed}})
159
      )
160
161
    assert %{state: :not_accepted, type: :failed, reasons: [:verifier_failed]} =
162
             AcceptedOutcome.public_projection(refusal)
163
  end
164
165
  test "human-only work and agents-disabled repositories stay outside the contract" do
166
    assert {:not_applicable, :human_only_work} =
167
             AcceptedOutcome.evaluate(claim(%{actor: :human}))
168
169
    assert {:not_applicable, :agents_disabled_repository} =
170
             AcceptedOutcome.evaluate(claim(%{agents_enabled: false}))
171
  end
172
end

This page updates live while a promote is in flight · changelog