> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aethis.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Provenance and citations

> Bind each criterion to the document it comes from, verified verbatim against retained bytes at publish time.

<Warning>
  **Authoring is invite-only private beta.** Publishing with citations needs an
  Aethis API key with authoring scopes.
  [Request access](https://aethis.ai/developer-access), or start on the open
  evaluate tier with [your first decision](/getting-started/first-decision) —
  no key required. The two tiers are set out on
  [Capabilities and access](/reference/capabilities).
</Warning>

A criterion can declare **citation keys** — opaque labels such as
`BNA1981#Schedule1/P1.1` — in its `source_refs`. A key on its own says nothing:
it is a placeholder for "this rule comes from somewhere". Provenance is the step
that resolves those placeholders into **verified references**: at publish time
you supply, per key, the document it cites and the verbatim words it relies on.
The engine fetches or loads the document, digests the exact bytes, checks the
quote occurs in them, and records an immutable reference on the published cut.

If any declared key fails, **the whole publish is rejected** and nothing is
stored — including the references that did verify. Provenance fails closed.

## The two kinds of citation target

Each entry names **exactly one** target. Both or neither is a `422`.

|                                 | `url`                                                                                                                       | `artefact_source_id`                                    |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| What it cites                   | A public HTTPS document                                                                                                     | A file you uploaded to the project                      |
| How it resolves                 | Fetched at publish time (HTTPS only; no credentials, IP literals or private destinations; bounded redirects, size and time) | Loaded from the retained bytes — **zero network calls** |
| Retained bytes                  | The engine keeps a snapshot of what it fetched                                                                              | The uploaded artefact itself                            |
| Reference schema                | **v1**                                                                                                                      | **v2**                                                  |
| Who can read the document       | Anyone, at the cited URL                                                                                                    | Only a key with `projects:read` on that project         |
| Allowed on a **public** ruleset | Yes                                                                                                                         | **No** — rejected on every lane                         |

The split exists because the two answer different questions. A URL citation
says *"this rule comes from that public authority, and here is the text it
relies on"*. An artefact citation says *"this rule comes from the document I
gave you, and you still have it"* — the guidance PDF, the internal policy note,
the operator manual that has no public URL to point at.

## Publish with citations

### From the CLI

`aethis publish --source-targets` takes a YAML (or JSON) file keyed by citation
key. Each entry names `url` **or** `file`; a `file` entry is uploaded to the
project first and cited by the `source_id` it comes back with.

```yaml theme={null}
# targets.yaml
"BNA1981#Schedule1/P1.1":
  url: https://www.legislation.gov.uk/ukpga/1981/61/schedule/1
  title: British Nationality Act 1981, Schedule 1
  authority: UK Government
  licence: OGL-UK-3.0
  locator: Paragraph 1(1)(a)
  quote:
    exact: "is of full age and capacity"

"HO-GUIDE#4.2":
  file: ./corpus/naturalisation-guidance.pdf
  title: Naturalisation booklet AN
  authority: Home Office
  licence: OGL-UK-3.0
  quote:
    exact: "You must have been resident in the UK"
```

```bash theme={null}
aethis publish --source-targets targets.yaml
```

Everything checkable without the network is checked **before the first API
call** — exactly-one-target, the required `title` / `authority` / `licence` /
`quote.exact`, a readable file, an HTTPS URL, and unknown keys (a silently
ignored typo is how a citation loses its locator on an immutable ruleset). A
malformed targets file costs zero round trips.

The CLI then prints what each key resolved to, keeping the two kinds visibly
distinct — a `url` line is a public link, an `artefact` line is an
authenticated download:

```
Source targets (2)
  BNA1981#Schedule1/P1.1  url https://www.legislation.gov.uk/ukpga/1981/61/schedule/1
    fetched and snapshotted at publish — public link
  HO-GUIDE#4.2  artefact src_8CzLVwyx53rTGEJv (naturalisation-guidance.pdf)
    uploaded snapshot, verified at publish — authenticated download, never anonymously readable
```

A file entry whose bytes match a source already on the project reuses that
upload rather than duplicating it, and if the publish itself fails the uploads
are not rolled back — retrying reuses them by digest.

<Note>
  `--source-targets` needs `aethis-cli` **v0.31.0+**. Check what PyPI serves
  today on [Capabilities and access](/reference/capabilities) — if it is still
  behind, use the REST call below, which is what the CLI sends.
</Note>

### Over REST

`source_targets` is a map from citation key to target, on the publish body
*(illustrative — identifiers are placeholders; field names and shapes are
exact)*:

```bash theme={null}
curl -X POST https://api.aethis.ai/api/v1/public/projects/{project_id}/publish \
  -H "Content-Type: application/json" \
  -H "x-api-key: $AETHIS_API_KEY" \
  -d '{
    "slug": "acme/naturalisation/residence",
    "source_targets": {
      "BNA1981#Schedule1/P1.1": {
        "url": "https://www.legislation.gov.uk/ukpga/1981/61/schedule/1",
        "title": "British Nationality Act 1981, Schedule 1",
        "authority": "UK Government",
        "licence": "OGL-UK-3.0",
        "locator": "Paragraph 1(1)(a)",
        "quote": { "exact": "is of full age and capacity" }
      },
      "HO-GUIDE#4.2": {
        "artefact_source_id": "src_8CzLVwyx53rTGEJv",
        "title": "Naturalisation booklet AN",
        "authority": "Home Office",
        "licence": "OGL-UK-3.0",
        "quote": { "exact": "You must have been resident in the UK" }
      }
    }
  }'
```

Two behaviours worth knowing before you rely on the response:

* **Only keys the compiled ruleset declares are resolved.** A target whose key
  no criterion declares is ignored, not rejected — so a typo'd key publishes
  successfully with that citation simply missing. Read the ruleset back with
  `/explain` and check the keys landed. (The CLI does this for you and reports
  the count.)
* **A private publish that supplies `source_targets` resolves them now.** The
  references are recorded on the published cut, so a later public flip finds a
  citation-complete ruleset instead of a dead end. Supplying no targets on a
  private publish leaves the keys unresolved, which is fine until something
  needs them.

You can also supply `source_targets` on `PATCH /rulesets/{ruleset_id}/visibility`
when flipping a ruleset public — the flip resolves and validates them
fail-closed before it proceeds.

## What a stored reference looks like

Published references come back on every criterion from
`GET /rulesets/{ruleset_id}/explain`.

A **schema v1** reference — an HTTPS citation *(illustrative)*:

```json theme={null}
{
  "schema_version": 1,
  "source_id": "BNA1981#Schedule1/P1.1",
  "title": "British Nationality Act 1981, Schedule 1",
  "authority": "UK Government",
  "url": "https://www.legislation.gov.uk/ukpga/1981/61/schedule/1",
  "locator": "Paragraph 1(1)(a)",
  "licence": "OGL-UK-3.0",
  "content_digest": "sha256:1601f16d…ac038c51",
  "snapshot": "sha256:1601f16d…ac038c51",
  "verified_at": "2026-07-28T12:43:48.141458Z",
  "quote": { "exact": "is of full age and capacity" },
  "media_type": "html",
  "deep_link": "https://www.legislation.gov.uk/ukpga/1981/61/schedule/1#:~:text=is%20of%20full%20age%20and%20capacity"
}
```

A **schema v2** reference — an uploaded-artefact citation *(illustrative)*:

```json theme={null}
{
  "schema_version": 2,
  "target_kind": "artefact",
  "source_id": "HO-GUIDE#4.2",
  "title": "Naturalisation booklet AN",
  "authority": "Home Office",
  "artefact_project_id": "proj_8CzLVwyx53rTGEJv",
  "artefact_source_id": "src_8CzLVwyx53rTGEJv",
  "url": "/api/v1/public/projects/proj_8CzLVwyx53rTGEJv/sources/src_8CzLVwyx53rTGEJv/raw",
  "licence": "OGL-UK-3.0",
  "content_digest": "sha256:9f2b0c41…7d13ae60",
  "verified_at": "2026-07-28T12:43:48.141458Z",
  "quote": { "exact": "You must have been resident in the UK" },
  "media_type": "pdf",
  "deep_link": "/api/v1/public/projects/proj_8CzLVwyx53rTGEJv/sources/src_8CzLVwyx53rTGEJv/raw#page=7"
}
```

The fields that carry the weight:

| Field                                                      | What it is                                                                                                                                                                                                                                             |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `schema_version`                                           | `1` for a URL target, `2` for an artefact target. Evolution is additive: v1 references stored before this feature are served byte-for-byte unchanged, and a v1 consumer tolerates the v2 fields.                                                       |
| `content_digest`                                           | `sha256:<hex>` of the exact bytes the quote was checked against.                                                                                                                                                                                       |
| `quote.exact`                                              | The verbatim words, never a summary. Optional `prefix` / `suffix` give locating context.                                                                                                                                                               |
| `deep_link`                                                | Self-locating link derived from `url` + `quote`: a `#:~:text=` fragment for HTML and text, `#page=N` for PDFs when the locator names a page.                                                                                                           |
| `snapshot`                                                 | **v1 only, optional.** The digest keying the engine-retained snapshot of the fetched bytes (equal to `content_digest`). Absent on references stored before snapshot-on-fetch, and on v2 references — whose retained bytes *are* the uploaded artefact. |
| `target_kind`, `artefact_project_id`, `artefact_source_id` | **v2 only.** Absent on v1, where the target kind is implicitly `url`.                                                                                                                                                                                  |

### `url` on a v2 reference is a relative, authenticated path

<Warning>
  On a **v2** reference, `url` is **not** a public link. It is the *relative*
  path of the authenticated download route —
  `/api/v1/public/projects/{project_id}/sources/{source_id}/raw` — which you
  resolve against the engine base URL you are calling, and which requires an
  API key with the `projects:read` scope. Rendering it as a clickable public
  link is wrong twice over: it is not absolute, and it is not anonymous.
</Warning>

Fetching the retained artefact, then, is:

```bash theme={null}
curl -sS -D - -o naturalisation-guidance.pdf \
  -H "x-api-key: $AETHIS_API_KEY" \
  "https://api.aethis.ai/api/v1/public/projects/{project_id}/sources/{source_id}/raw"
```

The body is byte-identical to the upload, and `X-Source-SHA256` carries its
digest — verify it against the reference's `content_digest` and you have proven,
without trusting the engine, that you are holding the bytes the quote was
checked against. `404` means the source predates artefact retention (only its
extracted text survives); `500` means the source records a digest but its
artefact is missing, which is an integrity error reported loudly rather than
disguised as a legacy upload.

Uploaded sources are also protected from disappearing under a live citation:
`DELETE` on a source that any persisted ruleset cites returns `409`. Mark it
`superseded` or `reference_only` instead — it drops out of generation while its
citations stay resolvable.

## Public rulesets cite URLs only

A public ruleset's references are served anonymously, and an artefact reference
points at a private project source. So an artefact-backed ruleset can never
become publicly resolvable. The engine rejects it — with reason code
`artefact_reference_public_visibility_forbidden` — on **every** lane to public:

* publishing a ruleset that will be public,
* `PATCH /rulesets/{ruleset_id}/visibility` to `public`,
* `PATCH /rulebooks/{rulebook_id}/visibility` to `public` (a public rulebook
  would expose a private member's references),
* attaching or promoting a member into an already-public rulebook.

The sweep covers **stored** references too, not just the ones in the request: a
ruleset that ever published a v2 reference cannot later republish straight to
public, because retired cuts stay readable. Publish a fresh ruleset citing
public HTTPS URLs, or keep the ruleset private.

## What verification does and does not attest

The check is **verbatim after whitespace normalisation**. Runs of whitespace
collapse to single spaces on both sides; HTML is tag-, script- and
style-stripped, because a quote cites prose rather than markup; PDFs are checked
against extracted page text. There is no stemming, no case folding and no fuzzy
matching — the quote's words, in that order, must appear exactly.

<Warning>
  A verified reference attests **the retained snapshot at publish time**, and
  nothing else. It proves that on `verified_at`, bytes with that
  `content_digest` contained that text at that authority. It does **not**
  assert that the provision is still in force, that the guidance has not been
  withdrawn, or that the rule is a correct reading of it. Currency and legal
  validity remain a human responsibility.
</Warning>

The snapshot is what makes a citation survive its source. When a page is
rewritten or a URL goes dead, the quote and the digest still resolve to the
bytes the engine kept, so a decision made a year ago remains explainable.
Identical bytes are stored once, keyed by digest.

Pin the bytes you reviewed with `expected_digest` on a target: a fetch whose
digest differs then fails the publish rather than silently citing a changed
document.

## When a citation fails

Every failure is reported per key, with a stable `reason_code`, in a `422`
carrying `reason_code: source_reference_resolution_failed` and a `failures`
array. Nothing is published and no snapshot is retained.

| `reason_code`                                    | What happened                                                                                                    |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `unresolved_reference`                           | A criterion declares this citation key and no target was supplied for it                                         |
| `quote_not_found`                                | The verbatim quote does not occur in the fetched or retained bytes                                               |
| `digest_mismatch`                                | The fetched digest differs from the `expected_digest` you pinned — the source changed under the citation         |
| `pdf_extraction_failed`                          | The PDF could not be parsed into text, so the quote could not be checked                                         |
| `artefact_not_found`                             | No such uploaded source in this project (a cross-tenant id is deliberately indistinguishable from a missing one) |
| `artefact_cross_scope`                           | The source exists for your tenant but belongs to a different project                                             |
| `artefact_integrity_mismatch`                    | The source row, the stored artefact and a re-hash of its bytes do not all agree                                  |
| `artefact_unsupported_type`                      | The upload's file type cannot be quote-checked                                                                   |
| `legacy_source_no_artefact`                      | The source was uploaded before artefact retention, so there are no bytes to verify against — re-upload it        |
| `artefact_reference_public_visibility_forbidden` | The operation would make an artefact-backed ruleset publicly resolvable                                          |

URL targets additionally fail closed on the fetch itself — a non-HTTPS scheme,
credentials in the URL, an IP literal or private destination, too many
redirects, an oversized body or a timeout each carry their own reason code.

## Reading citations back

```bash theme={null}
curl -sS "https://api.aethis.ai/api/v1/public/rulesets/aethis%2Fuk-fsm%2Fchild-eligibility/explain"
```

Every criterion carries `source_refs` (the declared keys) and
`source_references` (the resolved, verified references):

```json theme={null}
{
  "ruleset_id": "uk-fsm-child-eligibility:20260528-244428c1",
  "slug": "aethis/uk-fsm/child-eligibility",
  "ruleset_version": "v2",
  "content_digest": "sha256:1601f16d8fdedf4fe8fcd08e33d65c2d20176aed87f7a7d573ca1e35ac038c51",
  "criteria": [
    {
      "criterion_id": "age_at_least_4",
      "group": "age_check",
      "title": "Child is aged 4 or over at start of academic year",
      "rule_text": "child.age is at least 4",
      "source_refs": null,
      "source_references": null
    }
  ]
}
```

<Note>
  Both fields are served by `api.aethis.ai` today, and the response above is
  what that call returns right now. The first-party showcase rulesets do not yet
  declare citation keys, so nothing resolves against them and both fields come
  back `null` — the reference shapes shown earlier on this page are taken from
  the live API schema, not from a showcase response. Treat the array as optional
  in client code: `null` and `[]` both mean "this criterion carries no verified
  reference", never "verification failed".
</Note>

## See also

* [Rule generation](/authoring/rule-generation#grounding-report) — the
  generation-time grounding report, which is a *different* thing: it scores how
  well generated rules trace to your uploaded sources, and never blocks. The
  citations on this page are publish-time, verified, immutable and fail-closed.
* [CLI](/interfaces/cli#publish-with-citations) — the `--source-targets` flag.
* [REST API](/interfaces/rest-api#scopes) — the `projects:read` scope that the
  artefact download route requires.
* [Decision envelope](/concepts/decision-envelope) — the per-decision
  reproducibility fields that sit alongside provenance.
