# Acceptances

Some changes can break someone else, such as removing a field clients
read. `jadpo check` fails on them until a person accepts them. An
acceptance is one file per **decision**, listing every change it allows,
with the reason in a `///` line. This one allows removing the `priority`
field from what API `main` sends as a `TodoView`:

`acceptances/20261020T1400_remove_todo_priority.jadpo`:

```jadpo
/// Priority was never shown by any client; the mobile app stopped sending it in 3.2.
acceptance {
    changes: {
        main.TodoView.priority: ApiChange.output_field_removed
    }
}
```

`acceptance`, `changes:` and `ApiChange` are built in. The rest of the entry
comes from the compiler: each entry is `subject: Kind.change`, with the
subject and the change copied from its message. A subject is a dotted name,
or, for a route, its API, method and path in quotes, since a route has no
name. You don't need to type one, since `jadpo acceptance new` drafts the
file (below).

- **It counts only in the change that adds it.** Acceptances already on
  the before side are history, and editing one is an error
  ([`acceptance.edited`](/docs/errors/acceptance.edited.md)). `check` fails
  when a change needs a person and no added acceptance lists it
  ([`acceptance.missing`](/docs/errors/acceptance.missing.md)), when a
  listed entry matches nothing
  ([`acceptance.unmatched`](/docs/errors/acceptance.unmatched.md)), or when
  the reason is missing
  ([`acceptance.no_reason`](/docs/errors/acceptance.no_reason.md)). A file
  that isn't one acceptance with its `changes:` is
  [`acceptance.invalid`](/docs/errors/acceptance.invalid.md). A quoted key
  anywhere else is a syntax error.
- **Drafting is a command.** `jadpo acceptance new <purpose>` writes a
  file listing every change not yet accepted, with no reason. An agent
  may run it, then stops: the missing reason keeps `check` failing until
  a person writes one.
- **The git host is the gate.** `acceptances/`, `api/`,
  `access.jadpo_record` and `network.jadpo_record` are owned in
  CODEOWNERS, so the owner's approval of the pull request is what lets a
  decision in, and the host records who gave it
  ([Review and sign-off](/docs/review-and-sign-off.md)). `jadpo new`
  writes a `CODEOWNERS` listing them, for the project to name its owners.

| Needs a person | Kind | Where it is described |
|---|---|---|
| A route removed, a required input field added, a rule tightened, an output field removed or retyped, a body sent as another media type, a failure code changed | `ApiChange` | [The public API record](/docs/the-public-api-record.md) |
| An event removed, or an event's field removed, retyped, newly required or its rule tightened | `ApiChange` | [The event record](/docs/declaring-an-event.md#the-event-record) |
| A column or table dropped, an entity removed, an entity moved to another store | `DataChange` | [Removing data](/docs/migrations.md#removing-data) |
| Any widening of who can do what, however it arises | `PolicyChange` | [Policy](/docs/policy.md) |
| A sign-in setting loosened past its limit | `PolicyChange` | [Magic links](/docs/magic-links.md#settings) |
| Any change to how a role must be signed in to, tighter or looser, or a role's line added or removed | `PolicyChange` | [Roles that need a way in](/docs/a-membership-role.md#roles-that-need-a-way-in) |
| A git location added to, or widened in, `packages:`'s `own_sources:` | `PolicyChange` | [Installing safely](/docs/where-packages-come-from.md#installing-safely) |
| A domain created or renamed | `DomainChange` | [Domains](/docs/domains.md) |
| A risky package change ([Package upgrades](/docs/package-upgrades.md)) | `PackageChange` | [External services](/docs/external-services.md) |
| A package's service: a new or changed container image, before it runs | `PackageChange` | [External services](/docs/external-services.md) |
| Installing a third party's package from git rather than the registry (an unverified publisher) | `PackageChange` | [External services](/docs/external-services.md) |
| A connection that receives personal data but can't erase it, with the reason the data stays | `ErasureNotPossible` | [External services](/docs/external-services.md), [Erasure](/docs/erasure.md) |
| A threshold raised or removed | `ThresholdChange` | [Monitoring](/docs/monitoring-thresholds.md) |
| Declining an error the compiler's catalogue marks as a security rule, such as `policy.call_ungranted` | `PolicyChange` | [Writing a decline](/docs/declining-a-judgement-call.md#writing-a-decline) |
| A new outside host the app can send data to: a provider's host, a connection's `host:`, an `outbound:` grant or an email relay | `NetworkChange` | [External services](/docs/external-services.md) |

Changes that only add need no acceptance: a new route, even one anyone
may call, a new optional input field, a new output field, tightened
policy, listing a query, a new entity that belongs to a tenant. The diff
still shows them.

Each change the compiler reports, and the subject it names:

| Change | Subject | When |
|---|---|---|
| `ApiChange.route_removed` | `"main GET /todos/{todo_id: Uuid}"` | A route's entry in the API record is gone |
| `ApiChange.input_field_required` | `main.CreateTodo.due` | A field clients must send is new on a type they send, or on an event |
| `ApiChange.rule_tightened` | `main.CreateTodo.title` | A field clients send allows fewer values: a bound raised or lowered, a rule added, a type changed, `none` no longer allowed, or a field that could be left out no longer can; an enum variant gone from what clients send |
| `ApiChange.output_field_removed` | `main.TodoView.priority` | A field clients read is gone, from its type or from the type a route now answers with |
| `ApiChange.output_field_retyped` | `main.TodoView.priority` | A field clients read allows more or other values |
| `ApiChange.media_type_changed` | `"main GET /deeds/{deed_ref: Text}"` | A route's `sends` line in the API record is new, gone or names another media type ([Sending raw bytes](/docs/output-and-status.md#sending-raw-bytes)) |
| `ApiChange.failure_changed` | `main.todo_locked` | A failure code is gone, or its status changed |
| `ApiChange.event_removed` | `planning.TodoCompleted` | An event is gone from its domain's event record |
| `PolicyChange.route_public` | `"main GET /books"` | A route that needed a signed-in caller is open to anyone |
| `PolicyChange.confirmation_removed` | `"main POST /payouts/{payout_id: Uuid}/send"` | A route that needed the person's passkey no longer asks for it ([Confirm one request with a passkey](/docs/routes-signed-in-by-default.md#confirm-one-request-with-a-passkey-confirm-confirmationpasskey)) |
| `PolicyChange.widened` | `library.Book.read`, `library.Book.cost.read`, `lending.borrow` | A caller is added to an entity's or a field's grant, or to a listed action's or query's line; or a field's own list now follows its record's |
| `PolicyChange.restriction_removed` | `library.Book.cost` | A field's own row, or its `server_only`, is gone |
| `PolicyChange.tenancy_loosened` | `library.Book`, or `tenant` | An entity that belonged to the tenant is shared by every tenant, or the app's tenant is gone |
| `PolicyChange.shared_entity_added` | `library.Genre` | A new entity shared by every tenant |
| `PolicyChange.listing_added` | `lending.borrow` | An action is newly listed under `actions:`, which replaces its entities' grants |
| `PolicyChange.listing_removed` | `catalogue.staff_books` | An action's or a query's line under `actions:` is gone |
| `PolicyChange.call_ungranted_declined` | `lending.borrow` | A new `decline policy.call_ungranted:`, on the declaration it sits above |
| `PolicyChange.sign_in_loosened` | `magic_link.expires_after` | A setting past its limit and looser than before |
| `PolicyChange.role_requirements_changed` | `desk.StaffRole.admin` | A role's `requires:` line changed, tighter or looser, or a role's line added or removed |
| `PolicyChange.own_source_added` | `"github.com/acme/"` | An entry in `packages:`'s `own_sources:` that wasn't there, or one that now covers more; not one below an entry already there, which lets nothing new in |
| `DomainChange.created` | `billing` | A folder under `domains/` that wasn't there |
| `NetworkChange.host_added` | `couriers` | A connection reaches a host it didn't, a new connection's included; one entry covers every new host of the connection |
| `PackageChange.git_source` | `AcmeCouriers.v1` | A package locked from git that wasn't, or locked from another repository than it was, from a repository under none of `packages:`'s `own_sources:` |
| `DataChange.column_dropped` | `Todo.legacy_flag` | A migration this change adds drops a retired column, by the field's born name, as `changes.jadpo` names it |
| `DataChange.table_dropped` | `Todo` | A migration this change adds drops a retired table, by its entity's born name |
| `DataChange.entity_removed` | `Todo` | A migration this change adds retires an entity's table, since the entity is gone |
| `DataChange.entity_moved` | `Todo` | A migration this change adds gives an entity a table in another store, leaving its records behind |
| `PackageChange.operation_removed` | `AcmeCouriers.bookings.get` | An operation, action or library function the app used is gone ([Package upgrades](/docs/package-upgrades.md)) |
| `PackageChange.declaration_removed` | `Postcodes.UkPostcode` | A type, failure or message the app wrote is gone |
| `PackageChange.operation_changed` | `AcmeCouriers.bookings.get` | A used operation's request, input, output or what it changes, or a used function's or action's signature, is different |
| `PackageChange.field_removed` | `AcmeCouriers.Booking.driver_phone` | A field or enum variant of a type the app uses is gone |
| `PackageChange.field_retyped` | `AcmeCouriers.Booking.weight`, or `Postcodes.UkPostcode` | A field of a type the app uses has another type or rules; or a used refined type's base or rules, or a used failure's code or kind, changed |
| `PackageChange.failure_added` | `AcmeCouriers.bookings.create.address_unverifiable` | A used operation, action or function can fail in a new way, named by the failure's code |
| `PackageChange.host_changed` | `AcmeCouriers.host` | A connected provider package's `hosts:` changed; one entry covers them all |
| `PackageChange.credential_changed` | `AcmeCouriers.credential` | A connected provider package's credential scheme changed |
| `ErasureNotPossible.data_stays` | `couriers` | A connection receives personal data and can't erase it ([Erasure at providers](/docs/erasure-at-providers.md)), with no acceptance on the before side listing it |
| `ThresholdChange.raised` | `planning.todo_list.p95`, `planning.todo_list.within` | A threshold's limit is higher than the same-named threshold's on the before side, or its `within:` longer |
| `ThresholdChange.removed` | `planning.todo_list.p95`, or `planning.todo_list` | A limit is gone from a threshold; or the threshold is gone, or watches something else |
| `ThresholdChange.lowered` | `uploads.free_space_floor` | A disk store's `free_space_floor:` is lower than it was |

Two records give these checks a before side, and put what changed in
front of the reviewer: `access.jadpo_record`, who can do what, from the
permissions report, and `network.jadpo_record`, every outside host the
app can reach, from the network report. `jadpo check` keeps them at the
root beside `api/`, as it keeps the API record, and `jadpo records update`
writes them. A project with nothing for one to hold has no such file,
except that an app with a `connections:` block keeps
`network.jadpo_record`, its header alone while it reaches no host.

```text
// Generated by Jadpo from who can do what; update it with `jadpo records update`, never by hand.
record access 1

tenant Company

entity work.Project
    create CompanyRole.editor
    read CompanyRole.editor
    read CompanyRole.viewer
    tenant company_id

field work.Project.budget
    read CompanyRole.editor

action todos.transfer_todo
    who Todo.owner_id

decline policy.call_ungranted lending.borrow

sign_in magic_link
    code_attempts: 5
    expires_after: 15m
    failed_codes_per_day: 20
```

```text
// Generated by Jadpo from the hosts the app can reach; update it with `jadpo records update`, never by hand.
record network 1

connection couriers
    for AcmeCouriers.v1
    host https://api.acme-couriers.example
    host https://sandbox.acme-couriers.example

connection main
    for database
    host config.database_url
```

A field's own list that lets nobody in is written `read nobody`, so it
reads apart from a field that follows its record's list. A route made
public is read from the API record, whose route entry says
`authentication none`.
