# Reading a verdict

A check writes one JSON file. The Action writes it at its `output` path. The API returns it as the response body.

There is no overall pass or fail. Each obligation has its own reading. The only summary is the policy exit, which follows the `fail-on` setting you chose.

## 1. The verdict envelope

Schema `assure.serve.verdict/v1`.

| Field | Meaning |
|---|---|
| `schema` | Always `assure.serve.verdict/v1`. |
| `check_id` | A random 32-character hex id for this check. |
| `profile` | The profile that ran, for example `postgresql-observed-baseline`. |
| `profile_version` | The version of that profile's checker entry. |
| `engine` | `commit`, `release_sha256` and `checkers_sha256`: the exact engine that produced the readings. `GET /v1/profiles` publishes the serving build's `commit` and `release` (its `release_sha256`) at the top level of the listing, and the free summary (`GET /v1/checks/{id}/summary`) carries `engine.commit` and ends its `headline` with `[engine <first 12 hex digits>]`, so a 'before' and an 'after' reading show at a glance whether the same deployed engine answered. |
| `input` | `files`: each file's `path`, `bytes` and `sha256`. `bundle_sha256`: one digest over that list. |
| `outcome` | `verdict` when at least one obligation reads holds, fails or deviates (holds or fails for the declared profile). `no_verdict` otherwise. A vacuous reading alone is not a verdict: it says only that a domain was empty. |
| `charged` | Whether the server counted this as a charged check: `true` when it did and `false` when it did not. This field is not an amount spent: a valid free-profile check can carry `charged: true` with zero monetary cost. Newly run API verdicts, `verdict` and `no_verdict` alike, state this field; a failure states it beside `outcome` too. The shared outcome table and refusal detail supply the classification, while the API records its actual decision: a refusal before admission, the checker's own internal error, a server restart, and a malformed `python-auth-declared-model` bundle refused by its free pipeline are not charged. The outcome alone cannot settle every case. Stored readbacks and idempotent replays retain the original field and make no new charge. Historical stored answers can lack `charged`; that is unknown, not `false`. Local verdicts and failures say `false`. An Action failure can say `null` when a sent check's charge could not be read, as described below. |
| `readings` | The checker's full output, unchanged. This is the proof tree (section 3). |
| `counts` | The number of obligation readings in each status. Every status appears, including those at zero. A refused machine's own line is not an obligation reading and is not counted here: the sum of `counts` equals `completion.usable_readings.of`. |
| `machines_refused` | `count`: how many machines the checker refused, that is `len(machines.refused)` on every profile (the observed profile reports its refusals through its readings and writes no refusal row); `by_status`: that count by the refused machine's status, for example `{"not_evaluated": 1, "missing_premise": 1}`. `{count: 0, by_status: {}}` when no machine was refused. The count is gross: a machine a scope limit names is in it, while the first line's "n refused" and the summary's "Machines refused: n" line are net of the limited machines (section 9), so `count` less the limited machines is the number the first line prints. |
| `a0` | Null, or the reason the input failed its integrity gate (section 7). |
| `machines` | `read`: the machines that gave readings of their own. `refused`: each machine that could not be read, with the checker's `reason` and its `status`: the status of that machine's own refusal line in `rows` where it has one, so the multiset of these statuses is `machines_refused.by_status`; where it has none, the status its reason names (`representation: pg_hba.conf absent from raw` is `representation`), else the profile's not-collected status for a machine with no readings at all (`not_observed` on the observed profiles, `not_evaluated` on the declared ones). You never have to guess it. `total`: every machine the profile checks (8). `source`: `reported` when the checker said which machines it refused, `inferred` when Assure worked it out from the readings (section 7). |
| `rows` | One line per obligation: `machine`, `id`, `status`, `status_text`, `authority` and `reason`. A refused machine keeps its own line in place, with `id` null, `kind: machine_refusal`, its `status`, its `reason` as the checker wrote it, and `needs`: `{kind, key, action}`, where `kind` is `declaration` (something to declare), `input` (something to collect) or `method` (no input admits it yet), `key` names what, and `action` says it in one sentence (for a raw/pg_hba.conf the collection sidecar records as `missing` or `unreadable`, `action` is the two statements that let the collection role read the server's own parse of the file, `GRANT SELECT ON pg_catalog.pg_hba_file_rules TO <role>; GRANT EXECUTE ON FUNCTION pg_catalog.pg_hba_file_rules() TO <role>` (PostgreSQL refuses the view's read with the SELECT grant alone), `<role>` the collection role read from the envelope's readings and quoted as SQL writes it, or the words `the collection role` when no collection role is read; the readings' own `grant` text is used instead when it names both statements; status `unsupported` keeps `needs` null); `needs` is null when what admits that reason is not tabled yet, and the reason text is kept. On the declared profiles (`postgresql-declared-model`, `mysql-declared-model`) that line also carries `derive_notes`: the `{fact, written}` notes the derive wrote for that machine in its `derive_notes.json`, in the derive's order (`[]` when it wrote none for that machine or the file is absent or unreadable), so you can tell which input produced the refusal, for example `no protected table in snapshot` (`protected_tables` not declared, or `[]`) from `protected table orders absent from snapshot` (declared, but not in the catalog snapshot); obligation lines do not carry it. `rows` and `counts` keep the checker's raw statuses. A row the checker kept on a machine it refused is coverage: every rendered surface (the first line, the summary, the annotations, the not-established list, the report) reads it as not observed. |
| `policy` | `fail_on`: the statuses that fail the job (`not_collected` already expanded; `unresolved-security` as written). `exit` and `reason`: see section 9. `allow_partial`: as you sent it; it changes nothing. |
| `colour`, `bounds`, `not_established`, `undecided_by_cause`, `bias`, `completion` | The first line's colour, bounds and not-established list, that list by reason class (largest first, with each class's next step), the obligation classes, and the three completion answers: see section 9. |
| `observed_facts` | An object, `{}` unless the profile is `http-observed-baseline`. For each endpoint that was observed it holds `{endpoint id: the facts the readings used}` (the status code and the HSTS, CSP and cookie attributes), read from your uploaded `raw/HTTP-FACTS.json`, the file each reading's premise names. Its keys match each reading's `observations[].endpoint_id`. It is `{}` when that file is not the one the readings name. Cookie values and redirect URLs are never kept. |
| `scope_notes` | Limits that bound every reading in this check. See [SCOPE.md](/docs/scope). |
| `warnings` | A list, empty when there is nothing to warn about. Each entry is `{kind, text, ...}`: `text` is one sentence, and `kind` names the entry's other fields; new kinds can be added later, so read `kind` before you read the rest. The kinds today are `endpoint_non_success`, `known_limit`, `declared_ignored` and `dynamic_registration`. `endpoint_non_success` is on `http-observed-baseline`: an endpoint answered 4xx or 5xx, so the checker did not observe some policy checks for the whole run. Its entry is `{kind, endpoint_id, response_class, hidden_obligations, text}`, one per such endpoint, with `hidden_obligations` the sorted ids of the obligations left not observed on it. The summary shows each warning under the first line, and each is an annotation. A warning changes no status, count, colour or exit. Choose endpoints that answer HEAD with 2xx or 3xx. `known_limit`, on `postgresql-observed-baseline`, is `{kind, limit_id, obligation, machine, status, limits, text}`, one per served `RLS-1-OB` row of any status, with `limit_id` `OB-RLS1-ROUTE-KEYS-001`: a known limit of a served reading is disclosed beside the reading, and the reading stands as observed. `declared_ignored`, on `postgresql-observed-baseline`, is `{kind, file, key, text}`, one per key of `raw/declaration.json` the check dropped: any key other than `tls_floor`, `sensitive_roles`, `forbidden_methods` and `row_security_intent`, and one of those four whose value does not tighten or does not apply (a `tls_floor` that is not a TLS version name or is not above the pinned floor; a `sensitive_roles` or `forbidden_methods` that is not a list; a `row_security_intent` that is not a non-empty object mapping `"schema.table"` to `"deny_all"` or `"readable"`). An empty `sensitive_roles` or `forbidden_methods` list, or one whose members are all filtered out, produces no `declared_ignored` warning. `dynamic_registration`, on `python-auth-declared-model`, is `{kind, machine, obligations, text, locator}`, one per route registration the check refused as dynamic, with `obligations` the sorted ids of the readings it left not established; it carries `locator` only when the refusal's module names exactly one file of your repository. Any entry may carry an optional `locator`, `{path, line}` and nothing else: `path` is a relative repository path (letters, digits, `_`, `.`, `-`, up to 11 segments, no leading `.` or `-` in a segment) and `line` a positive integer. The summary's `warnings`, the annotations and the report page then show the entry as `<text> (<path>:<line>)`, so an action can cite `app/__init__.py:76`; an entry without one, or with a malformed one, shows its text alone. |
| seven flags | Fixed record flags: `execution_authorized`, `hardware_authorized`, `industrial_release_authorized`, `release_allowed`, `physical_validation`, `simulation`, `self_approved`. A verdict authorises no change, release or action. The flags are part of every API answer's contract (this file, the failure envelope, the check summary, the proof tree JSON), so a program reading one can see the record authorises nothing. A file Assure drafts for you to commit or read carries none: assure-requirements-draft.md, assure-requirements-typed.json, the job summary, the pull request comment and body, the annotations and the proof tree page. A drafted declaration keeps them because its schema names them; you need not change them. |

When no verdict can be made, the file is a failure envelope instead, schema `assure.serve.failure/v1`: `check_id`, `outcome`, `charged`, `reason`, `action`, `detail` and the same flags. An envelope newly built by the API also carries `build` `{commit, release_sha256, checkers_sha256}`, the engine that answered (section 11); an older stored failure is re-served as written without adding the block. A refusal answered before a check was admitted also carries `request_id` in the API's response body, the `X-Request-Id` of that request (issue #1101), the citation when `check_id` is null; the file the Action writes for a refusal is built on your runner and does not carry it. A typed failure of an admitted check has its `check_id` and no `request_id`. In `mode: api`, the Action copies the server's stated `charged` as is. It writes `false` when nothing reached the server, including a refusal before a check was sent or `mode: local`, which never contacts the API; it writes `null` when a check reached the server but its charge could not be read, for example `api_unreachable` after a 202 or an older answer without `charged`. Read `GET /v1/checks/{id}` with the check id in the file; an old stored answer can still lack the field, so do not infer a charge from its outcome. A `mode: local` verdict file, and the verdict and failure files of `python3.14 -m serve.check`, say `charged: false`. `detail.served_from` says whether the API minted it in the request or served the stored record (section 7). The quickstarts list every failure outcome. When the Action writes it, `detail.answered` names the request the API refused: `profiles` (the profile list, before anything is collected: a wrong or revoked key or a rate limit refused there sends nothing from your system), `check` (the upload) or `poll` (the request for a sent check's result).

What each top-level field of the JSON summary (`GET /v1/checks/{id}/summary`) varies by, `serve/check_summary.py` `VARIES` (QUICKSTART-API.md section 12 says how to read it): `run` (differs between two runs of the same input on the same build), `build` (the same on every run of one build, may differ at another) or `input` (changes only with the stored verdict or the request's `fail_on`). The summary carries this table itself as `varies_by` (`{field: run|build|input}`, one entry for each top-level key it carries, `varies_by` included), so a reader need not open the source.

| field | varies by |
|---|---|
| `schema` | build |
| `execution_authorized` | build |
| `hardware_authorized` | build |
| `industrial_release_authorized` | build |
| `release_allowed` | build |
| `physical_validation` | build |
| `simulation` | build |
| `self_approved` | build |
| `check_id` | run |
| `profile` | input |
| `check_path` | run |
| `engine` | build |
| `headline` | build |
| `coverage_line` | run |
| `warnings` | input |
| `scope_notes` | input |
| `bounds` | input |
| `coverage` | input |
| `machines_refused` | input |
| `machines` | input |
| `outcome` | input |
| `colour` | input |
| `policy` | input |
| `disproven` | input |
| `counts` | input |
| `requirements_count` | input |
| `requirements` | input |
| `top_action` | input |
| `not_established` | input |
| `shadow` | run |
| `varies_by` | build |

## 2. The nine statuses

Four statuses are verdicts. Five are not. A vacuous reading alone does not make a check's outcome a verdict, because it says only that a domain was empty.

| Status | Verdict | Meaning | How it is written |
|---|---|---|---|
| `holds` | yes | The observed system meets the obligation over a non-empty observed domain. | "holds", or "holds in database \<db>". It is bounded by its qualifiers. |
| `fails` | yes | An unconditional vendor requirement, or an unconditional safety property computed from observation, is broken. | "fails: \<id>", where the id is a baseline row or a safety property (such as OSP-AUTH-1), with " in database \<db>" on a database-scoped obligation |
| `deviates` | yes | A recommendation is not followed. `authority` says whose recommendation. | "deviates from vendor guidance \<row id>" or "deviates from the Assure baseline \<row id>" |
| `vacuous` | yes | The observed domain is empty. There is nothing to check. | "vacuous: observed empty domain", sometimes with " in database \<db>". It always carries `domain_locator`. |
| `needs_intent` | no | The rule depends on what the system is for. | "needs intent", or "needs intent in database \<db>"; its reason reads "needs intent: out of scope for Assure", with the observations listed |
| `not_observed` | no | A fact the reading needs was not collected or could not be read. | "not observed: \<what> (\<locator>)" |
| `missing_baseline` | no | A baseline row the reading needs is absent or not documented for this major. | "missing baseline: \<row id> not pinned for major \<n>" |
| `missing_method` | no | The checker has no method for this input shape. | "missing method: \<gap id>" |
| `representation` | no | The input could not be represented faithfully. | "representation: \<reason>" |

How to read them:

- **vacuous** means the domain was empty. It is a statement about emptiness. It is a different result from holds.
- **deviates** is a departure from a recommendation. It is a different result from fails. `authority: "vendor"` means the PostgreSQL documentation recommends it. `authority: "assure"` means the Assure baseline recommends it.
- **not observed** means the fact was missing. Nothing was checked for that reading.
- **needs intent** means the answer depends on your purpose, which Assure does not ask for. The observations are listed so you can judge.
- **missing baseline**, **missing method** and **representation** are limits of the checker or its input. They say nothing about your system.

### Precedence

When cases or parts of an obligation combine, the first status present in this list wins:

fails > deviates > representation > missing_method > not_observed > missing_baseline > needs_intent > holds > vacuous

All parts vacuous gives vacuous. Holds plus vacuous gives holds.

## 3. One reading

Readings sit at `readings.M<n>.obligations.<id>`. `readings.order` lists the 41 ids in profile order.

| Field | Meaning |
|---|---|
| `status` | One of the nine statuses. |
| `status_text` | The short text from the table above. Show it as given. |
| `reason` | The full reason. For not observed it names what is missing and where it was looked for. |
| `authority` | Present on deviates: `vendor` or `assure`. |
| `witnesses` | A list. For fails, deviates and needs intent: every example that shows it. Empty otherwise. |
| `qualifiers` | Strings that bound the reading, such as the database it covers. |
| `observations` | Observed facts that bear on the reading. |
| `premises` | Every input the reading used, each with its source (section 4). |
| `domain_locator` | For vacuous: where the empty domain was looked for. Null otherwise. |
| `statement` | The obligation, in words. |

## 4. Premises and their sources

Each premise has a `field`, a `source` and a `role` (`reads` or `informational`). The sources you will see:

| Source | Meaning |
|---|---|
| `observed` | Read from your system. It names the file (`artefact`), its `artefact_sha256` and a `locator` inside it. |
| `baseline` | A pinned public baseline row. It names the `row`, its digest and its `authority_class`: `vendor_documentation` or `assure_baseline`. |
| `declared` | A value you supplied in `raw/declaration.json`. It can only tighten a baseline. |
| `intent_required` | An input only you can give. The reading is "needs intent: out of scope for Assure". |
| `not_observed` | A fact that was missing. It names the gap. |

A separate list, `internal_premises`, holds method tags such as `model_constant` or `domain_restriction`. These are internal to the checker's method. They are neither observations nor baselines.

A verdict never reads a premise that is not observed or that needs intent.

## 5. The 41 obligations

25 are intent-free and 16 are intent-bound. Scope `db` means the connected database only. Scope `cl` means the whole cluster.

| Id | Machine | Class | Scope | Statement |
|---|---|---|---|---|
| PRIV-1 | M1 | intent-bound | db | Declared ordinary-ACL privilege queries are answered as expected. |
| PRIV-2-OB | M1 | intent-free | cl | No login other than an observed superuser or the collection role can become superuser or reach the server-file/program roles. |
| PRIV-3 | M1 | intent-bound | db | Roles declared disjoint share no ordinary ACL privilege. |
| PRIV-4 | M1 | intent-bound | db | No sensitive object has a PUBLIC ACL entry. |
| PRIV-5 | M1 | intent-bound | db | Default ACLs equal the expected ACLs. |
| PRIV-6 | M1 | intent-bound | db | Declared revokes leave no dependent grant. |
| GRANT-OPTION-POLICY | M1 | intent-bound | db | No sensitive-object grant option is held outside the administrators and the declared `table_owners` (issue #2570). |
| HBA-1-OB | M2 | intent-free | cl | No non-host-local TCP tuple is decided by trust, or by password on a non-hostssl record. |
| HBA-2 | M2 | intent-bound | cl | Every intended connection is accepted by its expected method and record. |
| HBA-3-OB | M2 | intent-free | cl | No configured pg_hba record is fully shadowed over the configured tuple domain. |
| HBA-4 | M2 | intent-bound | cl | Physical replication is admitted only for the operator's roles and standby addresses. |
| HBA-5-OB | M2 | intent-free | cl | Every record admitting a BL-ASSURE-TLS-PRIV-1 login from a non-host-local address is hostssl. |
| HBA-6 | M2 | intent-free | cl | The server's own pg_hba parse has no error rows and loaded rules equal the file (not observable in this version). |
| TLS-1-OB | M3 | intent-free | cl | Every server-originated channel uses verify-full. |
| TLS-2-OB | M3 | intent-free | cl | Every non-host-local record admitting a privileged login is hostssl and ssl is on. |
| TLS-3 | M3 | intent-bound | cl | Records required to use client certificates use cert or verify-full with ssl_ca_file set. |
| TLS-4-OB | M3 | intent-free | cl | With ssl on, ssl_min_protocol_version is at or above PG-BL-TLS-MIN-1. |
| TLS-5-OB | M3 | intent-free | cl | With ssl on, the key file is 0600, or 0640 root-owned (not observed while the mode is not collected). |
| REP-1-OB | M4 | intent-free | cl | Every admissible physical replication path uses non-trust auth off-host, an explicit role and a non-wide address off-host. |
| REP-2-OB | M4 | intent-free | cl | Every physical slot has a matching standby consumer with bounded retention. |
| REP-3-CONFIG | M4 | intent-bound | cl | Required no-loss durability selects a reachable synchronous standby. |
| REP-3 | M4 | intent-bound | cl | No committed transaction is lost on primary failure, when required. |
| RLS-1-OB | M5 | intent-free | db | No protected relation is read or written unfiltered by a non-superuser, non-BYPASSRLS, non-owner path. |
| RLS-2 | M5 | intent-bound | db | Required policies exist and imply the operator's tenant predicate. |
| RLS-3 | M5 | intent-bound | db | INSERT/UPDATE admission implies the operator's new-row predicate. |
| RLS-4-OB | M5 | intent-free | db | Owner-mode views over protected relations are listed, as is one over a relation observed (pg_inherits) to share rows with a protected relation while its own row filter is off. |
| RLS-5-OB | M5 | intent-free | db | Each table with row-level protection on has a SELECT policy. |
| SD-1-OB | M6 | intent-free | db | Every privileged SECURITY DEFINER function sets its own search_path, pg_temp last, no PUBLIC-writable schema. |
| SD-2-OB | M6 | intent-free | db | Privileged SECURITY DEFINER functions with PUBLIC EXECUTE are listed. |
| SD-3-OB | M6 | intent-free | db | No schema on a login's session search_path is CREATE-able by PUBLIC. |
| SD-4-OB | M6 | intent-free | db | The public schema has no PUBLIC CREATE. |
| SU-1 | M7 | intent-bound | cl | Observed superusers are exactly the operator's administrators. |
| SU-2-OB | M7 | intent-free | cl | Transitive memberships in the server-file/program roles are listed. |
| SU-3-OB | M7 | intent-free | cl | Transitive memberships in pg_read_all_data / pg_write_all_data are listed. |
| SU-4-OB | M7 | intent-free | cl | Every membership in the remaining predefined roles is a vendor-default edge. |
| LR-1 | M8 | intent-bound | db | A publication cannot be read by a replication login other than the exclusive reader. |
| LR-1-SCOPE | M8 | intent-bound | db | Published tables stay within the intended tables. |
| LR-2-OB | M8 | intent-free | db | run_as_owner is false unless each target owner equals the subscription owner. |
| LR-2-APPLY-OB | M8 | intent-free | db | Each subscription target can be applied as its owner. |
| LR-3-OB | M8 | intent-free | cl | Each subscription uses verify-full, password_required true, a non-superuser owner. |
| LR-4-OB | M8 | intent-free | db | Publisher roles with SUPERUSER or BYPASSRLS are listed. |

Every intent-bound obligation reads needs intent. The "listed" obligations read needs intent with the list when something is found, and vacuous when nothing is.

### CVE exposure rows

Besides the 41 obligations, a verdict whose inputs the checker could read carries one CVE row per table entry, and so does a run refused because the server's major is outside 14..18 or not observed (a 9.x or 10 server still gets its CVE-2018-1058 reading; the other rows read `representation`); any other whole-run refusal, except an unreadable PROFILE.json (no row is listed then), lists each CVE row as `representation` with the refusal's reason. Such a verdict carries one row per entry of the checker's CVE table (`CVE-TABLE.json`), under machine M8, with the CVE id as its row id. A row reads `fails` when `server_version_num` is in an affected major below its first fixed minor and the CVE's precondition is present (the reason names both, and `witnesses` carries both facts: first `{"fact": "server_version_num", "server_version_num": <n>, "version": <version>, "fixed": <minor>, "locator": "COLLECTION-SIDECAR.json#server.server_version_num"}`, then `{"fact": "precondition", "text": <precondition fact>, ...}`, which for CVE-2024-7348 adds `locator` (its `catalog_snapshot.json#schemas[nspname=<schema>]` pointer), `object` (`schema <name>`) and `grantee`, `privilege` and `item` for a CREATE grant or `owner` for a schema owner); `holds` when the version is fixed or unaffected, or the precondition is absent; `not_observed` when `server_version_num` or a fact the precondition needs was not collected (the reason names it). Each row is listed in the profile's `cve_rows`. A `fails` reading cites, as its premise, the baseline row its `cve_rows` entry names (`PG-BL-<CVE id>`, graded fails, citing the NVD CVE record and the fixed minors' release notes); its reason reads `fails: PG-BL-<CVE id> server_version_num <n>: <version> is below the fixed <minor>; <precondition fact>`. While that row is not pinned or not graded fails, an affected server with the precondition present reads `representation`, with the reason naming the version and the precondition fact and no witness; only a `fails` CVE row carries witnesses. A table entry with no precondition reads `missing_method`; one the profile does not list reads `representation`.

| Id | Affected majors (first fixed) | Precondition |
|---|---|---|
| CVE-2018-1058 | 9.3 (9.3.22), 9.4 (9.4.17), 9.5 (9.5.12), 9.6 (9.6.8), 10 (10.3) | PUBLIC may CREATE in a schema on the default search_path. |
| CVE-2024-7348 | 12 (12.20), 13 (13.16), 14 (14.13), 15 (15.8), 16 (16.4) | A non-superuser owns, or may CREATE in, a non-system schema pg_dump reads. |
| CVE-2023-2454 | 11 (11.20), 12 (12.15), 13 (13.11), 14 (14.8), 15 (15.3) | PUBLIC or a non-superuser role holds CREATE on the current database (a null `datacl` grants it to the owner only). |
| CVE-2023-39418 | 15 (15.4) | A relation has row-level security on (`relrowsecurity`). |
| CVE-2023-5870 | 11 (11.22), 12 (12.17), 13 (13.13), 14 (14.10), 15 (15.5), 16 (16.1) | A role that is not a superuser is a member of pg_signal_backend, directly or through another role. |
| CVE-2024-0985 | 12 (12.18), 13 (13.14), 14 (14.11), 15 (15.6), 16 (16.2) | A materialized view is owned by a non-superuser (a superuser or other higher-privileged role may refresh it concurrently). |
| CVE-2024-10976 | 12 (12.21), 13 (13.17), 14 (14.14), 15 (15.9), 16 (16.5), 17 (17.1) | A row-level security policy is observed (the database has used CREATE POLICY). |
| CVE-2025-8713 | 13 (13.22), 14 (14.19), 15 (15.14), 16 (16.10), 17 (17.6) | A table with row-level security is partitioned or in an inheritance hierarchy, or a view PUBLIC may not read reads a base table PUBLIC may not read. Column statistics are not collected; ANALYZE and autovacuum maintain them. |
| CVE-2026-14666 | 14 (14.24), 15 (15.19), 16 (16.15), 17 (17.11), 18 (18.6) | A relation has row-level policy enforcement on (`relrowsecurity` is true). |
| CVE-2026-14672 | 16 (16.15), 17 (17.11), 18 (18.6) | scram_iterations is not the default 4096 for some login: server-wide (configuration file, ALTER SYSTEM or command line) or for a role or database (`pg_db_role_setting`). A collected include target is read in place; an include the collector did not gather, or a line it withheld, reads `not_observed` unless a later source (ALTER SYSTEM or the command line) sets the value. |
| CVE-2026-14673 | 14 (14.24), 15 (15.19), 16 (16.15), 18 (18.6) | The amcheck extension is installed (17 is not affected). |
| CVE-2026-14681 | 17 (17.11), 18 (18.6) | pg_hba.conf has a `hostgssenc` record and `ssl` is on (a direct TLS connection may then match that record without GSSAPI encryption). A collected `include` target is read in place; with no `hostgssenc` record observed, an `include` the collector did not gather or a record it withheld reads `not_observed`. |
| CVE-2026-6471 | 14 (14.24), 15 (15.19), 16 (16.15), 17 (17.11), 18 (18.6) | Logical decoding is on (`wal_level` is `logical`) and a non-superuser holds REPLICATION. |
| CVE-2026-14676 | 18 (18.6) | The pg_stat_statements module is loaded (shared_preload_libraries lists it). |

## 6. An empty database

A new, default database reads mostly **needs intent** and **vacuous**. That means there is little to check. The 16 intent-bound obligations always need intent. An empty database has no ordinary logins, tables, definer functions or subscriptions, so many domains are empty.

On the official PostgreSQL 18 image the checker reads: needs intent 17, vacuous 14, holds 4, not observed 4, deviates 2, missing baseline 0, fails 0.

## 7. "No verdicts" and input integrity

- **No verdicts.** `outcome` is `no_verdict` when no obligation on a machine that was read reads holds, fails or deviates. A vacuous reading alone does not count, and neither does a reading on a machine that could not be read. No verdict is a coverage fact, never an exit of its own: the summary says "No verdicts", and when no obligation on the machines read holds, the first line says "nothing could be established (0 of N obligations hold)". Its colour and exit follow the policy in section 9. Look at the not-established lines to see what was missing and what would establish it.
- **Machines not read.** The first line counts the machines read and refused, for example "5 of 8 machines read, 3 refused". The count is out of every machine the profile checks (eight for both profiles) less any a scope limit names, which read "n out of scope" and leave both counts (section 9); a machine with no reading at all is refused with the reason "no readings for this machine". Each refused machine is listed with the checker's reason. The envelope counts them in `machines_refused` (`count`, `by_status`); the refused machine's own row in `rows` carries `kind: machine_refusal` and `needs`, what would admit it (section 1), and the summary adds one line under the status table, "Machines refused: n (\<status> k, ...). A refused machine is not an obligation reading and is not in the table above.", then under "Machines not read" each machine's status and what it needs. When that reason lists bound comparisons, for example `M2 domain bound exceeded (users 5 > 6 or databases 7 > 6)`, the summary keeps it as written and adds, in plain words, only the bounds that were exceeded: `in plain words: domain bound exceeded: databases 7 (cap 6)`. A refused machine bounds the result; a status you name in `fail-on` on its rows stops the job as on a read machine, and a reading the checker kept on it is read as not observed: a named `fails` or `deviates` does not stop the job on such a row, a named `not_observed` (or `not_collected`) does. When no machine could be read at all, row 3 can apply, subject to the prove-class and `never` exceptions in section 9. When `machines.source` is `inferred`, the checker did not say which machines it refused, the machine count on the first line ends "(inferred)" and the summary says so, whether or not a machine was refused, and the list was worked out from the readings: a machine counts as refused when every reading of it is representation or not observed with one same reason.
- **The selected scope.** A profile can check a set you select that is not a set of machines, for example a list of endpoints. Its verdict then carries `scope_observed` beside `machines`: `unit` and `label` (for example `endpoint` and `endpoints`), `expected` (every unit you asked to be checked), `observed` (the units that were observed), `refused` (each unit that was not, with its `reason`) and `source` (`bound` when the set came with your request, `row` when the profile fixes it, `reported` when only the checker named it). The set you send is fixed before the checker runs, so the checker's output can add to it but never remove from it. The first line starts with this scope, for example "1 of 2 endpoints observed; 5 of 5 machines read", and adds "(accepted scope reduced)" when a unit was removed from the scope you accepted. Each unit that was not observed is listed with its reason. An unobserved unit bounds the result like a refused machine; colour and exit follow section 9. When no unit was observed at all, row 3 can apply, subject to its prove-class and `never` exceptions. Both PostgreSQL profiles check machines: their verdicts carry `machines` alone.
- **Where a failure came from.** A failure envelope the API serves carries `detail.served_from` (issue #1665): `handler` when it was minted while answering that request (a refusal before admission, an unreadable stored record, or a check that ended without a result), `store` when it is the admitted check's stored failure, both when it is first answered and when it is read back from `GET /v1/checks/<check_id>` or replayed (a failure stored before the key existed is replayed as stored, without it). The failure file the Action writes carries the API's `served_from` when the API answered the refusal, as it carries the rest of `detail` beside `detail.answered`; a refusal the Action built on your runner before the API answered has none, and neither does the failure file `serve.check` writes.
- **No reading: input integrity.** When `a0` is set, the input failed its integrity gate. Every reading is representation, with the gate's reason. The gate triggers row 3, subject to the prove-class and `never` exceptions in section 9, and the summary says "No reading: input integrity". Fix the collection and run again.

## 8. The declared profile

`postgresql-declared-model` reads per machine against a declaration you supply. Its statuses are holds, fails, vacuous, missing method, missing premise, representation and not evaluated. Holds, fails and vacuous are its verdicts. **Missing premise** means the declaration lacks a value the model needs. **Not evaluated** means the machine was not run; the reason says why. Both profiles are served. A holds on a tenant filter that reads a session setting (such as `current_setting(...)` or `auth.uid()`) means isolation holds given the application sets the setting from an authenticated identity before any query; a direct database login can set it. The observed profile prints this condition in the reason; the declared profile applies it without printing it. In the job summary, each fails line shows its witness from `readings`, such as `witness: path postgres:direct, command SELECT, outcome allowed, ...` (at most 5 witnesses per line); the verdict file holds them all. On `postgresql-observed-baseline`, an M6 `deviates` line (such as SD-1-OB) is followed by the functions whose cases read deviates, with their count, one entry per function with its login roles as a list: `15 objects: <function> (roles <login>, <login>), ... and 10 more` (at most 5 named). Its warning annotation carries the same words after the reason, cut with `…` when the message passes 300 characters. A database with no definer function is read through one `invoker:<login>` case per login (SD-3-OB, SD-4-OB); those cases name no function, so their lines name no objects.

## 9. The policy exit

`policy.exit` is the one number a CI job reads, and `colour` the one word. `policy.reason` names why, and the job summary says it in plain words. The colour follows the policy conditions below. A `fails` reading on a read machine is red whatever `fail-on` says. A status you name in `fail-on` also turns the reading red and sets the exit. `policy.reason` names the cause of the exit and nothing else. One order:

| Order | `policy.reason` | `policy.exit` | Colour | When |
|---|---|---|---|---|
| 1 | `never` | 0 | as the readings give | `fail-on` is `never`. A prove-class obligation that is not proven still exits 1 (`not_proven`, row 5). |
| 2 | `bad_input`, `unauthenticated` | 2 | red | The request or input was refused (`bad_input`), or the API key is missing, unknown or revoked (`unauthenticated`): a failure envelope, never a verdict. |
| 3 | `could_not_look` | 3 | yellow | Nothing could be read at all: the input-integrity gate stopped the check (`a0` is set), no machine was read, or a profile with a selected scope observed none of its units. A prove-class obligation that is not proven keeps the colour red even then. |
| 4 | `disproven` | 1 | red | Something is disproven and your `fail-on` stops on it: a reading on a machine that was read has a status you fail on (`fails` by default; `deviates` when you name it). |
| 5 | `not_proven` | 1 | red | A prove-class obligation is not proven: no holds on a machine that was read. It exits 1 whatever its reading and under every `fail-on`, `never` included; when it reads fails and your `fail-on` names fails, the reason is `disproven`. |
| 6 | `unresolved_strict` | 1 | red | Under `fail-on: unresolved-security`, an obligation marked `security: true` is unresolved. Only the security obligations the pinned collector can observe count in n and m: an obligation the profile marks `observable_at_pin: false` is listed as not established, "not observable at collector pin \<pin>", and the first line says "strict: n of m security obligations observable at this pin". An intent-bound obligation (`class: intent_bound`) depends on what the system is for, which no collector observes. All security obligations without a verdict bind under strict, including one the pin cannot observe; the report names each and why. An intent-bound one is named "unresolved by your choice (strict)", reads "depends on what the system is for" and has the action "state it in requirements.md". n and m count observability only: an intent-bound obligation never counts in them and is never listed as not observable at the collector pin; m counts the intent-free security obligations. |
| 7 | `stopped_by_choice` | 1 | red | A reading has a status that is not a verdict and that your `fail-on` names (`not_observed`, or a status `not_collected` stands for). The job stopped on a status you named in fail-on; the colour above says whether anything is disproven. |
| 8 | `platform_owned` | 0 | red | Every disproof your `fail-on` names is platform-owned: a `fails` reading whose `findings` all carry `owner_class` `platform` or `extension` is excluded from the exit as someone else's to fix, the colour stays red, and the first line ends "(exit 0: platform-owned)". |
| 9 | `nothing_disproven` | 0 | green | Otherwise, refused machines, unobserved units and a run with no verdict included: within these bounds, nothing disproven. |

Row 9 is a statement about disproof, not about coverage. With `fail-on: fails`, at least one machine read, a clear input-integrity gate and no red findings, a partly observed scope with zero usable readings is still green, exit 0, `policy.reason` `nothing_disproven`: an endpoint observed beside one refused leaves every obligation row `not_observed` when a case on the refused endpoint reads `not_observed`, so `counts.holds` is 0 and `completion.usable_readings.count` is 0 while the colour stays green. Read `completion.usable_readings` before trusting green: a `count` of 0 with `coverage_complete` false means nothing was established, and the first line names what to collect. Row 3 (`could_not_look`) can apply when `a0` is set, when no machine was read, or when a profile with a selected scope observed none of its units. Observing a unit rules out only the no-observed-unit trigger: the input-integrity gate and whether any machine was read still matter. Row 3 also has the prove-class and `never` exceptions shown above. When the run could look, red can mean disproof, an unproven prove-class obligation, an unresolved strict obligation, or a status named in `fail-on`; it is green only when none of those red findings exists.

**Which statuses an obligation can reach.** `GET /v1/profiles` lists, at its top level as `served.<pid>.obligations`, each obligation of the observed PostgreSQL profile with `reachable`: the statuses it can reach on observed data, in precedence order, computed from the checker's PROFILE.json grades and the order in which its assembler reads them (the assembler's gates are not a PROFILE.json field; a test runs the checker to keep the two in step). Every obligation can reach `representation`: a bundle the checker cannot read, or an error in the checker, reads every obligation `representation`. An intent-bound obligation otherwise reaches only `needs_intent`; one the checker marks `needs_config: false`, including the two whose facts are not collected (`HBA-6` and `LR-3-OB`), stops before any explorer runs, so it reaches only `representation`, `not_observed`, `missing_baseline` and `vacuous` (`LR-3-OB` reads `vacuous` when `pg_subscription` is observed empty, and `missing_baseline` without its baseline row); any other reaches `vacuous` on an empty domain, `representation`, `missing_method`, `missing_baseline` and `not_observed` through typed failures, gaps and baseline rows, the grade of each of its conjuncts, `needs_intent` for an ungraded conjunct or a tighter declared norm, and `holds` unless every member of its domain is a violation. This is a bounded set of possible statuses: an obligation that needs configuration includes `needs_intent` even when it grades other conjuncts, because the checker profile does not enumerate every conjunct the assembler can read. For example, `SD-4-OB` can read it on the writers variant, and `TLS-4-OB` when the server meets the vendor TLS floor but is below a tighter declared floor. An obligation whose `reachable` lacks `holds` and `fails` never binds `fail_on` with `fails`, the default: every obligation of this profile is `refute`-class, so `not_proven` (row 5) never applies to it, and it turns the exit only through a status you name in `fail_on` yourself (rows 6 and 7). `RLS-4-OB` is one: every owner-mode view path it counts is graded `needs_intent`, so it reads `needs_intent` or `vacuous`, never `holds` or `fails`; `RLS-1-OB` reaches `fails` (OSP-RLS-1) and `holds`; `RLS-5-OB` reaches `fails` (OSP-RLS-2), `holds` and `vacuous`; `TLS-5-OB` grades no conjunct, so it reaches `holds` and `needs_intent` but not `fails`. 24 of the profile's 41 obligations reach neither.

A `fails` reading sets exit 1 only when at least one of its `findings` has `owner_class` `app` or `unknown`, or it carries no `findings` at all; a `fails` reading whose findings are all `platform` or `extension` is platform-owned and excluded from the exit, `policy.reason` `platform_owned`, and the summary lists it under "Platform-owned, someone else's to fix". A `fails` reading on a read machine is red whatever `fail-on` says. A status you name in `fail-on` also turns the reading red and sets the exit. A reading of any verdict status (fails, deviates, holds, vacuous) the checker kept on a machine it reports refused is a gap in coverage, never a disproof or a holds: every surface reads it as not observed. The refusal is on the first line; the row is listed as not established on that machine; a `not_observed` (or `not_collected`) you name in `fail-on` stops the job on it as on any not-observed reading; the summary shows it under its refused machine, "M6 refused — SD-4-OB kept by the checker, read as not observed", never under its status, and adds one line under the status table, "Readings the checker kept on refused machines and read as not observed: n.", while the table keeps the raw counts; the paid report reads it the same way. When your `fail-on` leaves a disproven status out (for example `fail-on: deviates`), the colour stays red, the exit is 0 with `policy.reason` `disproven`, and the first line ends "(exit 0 by your fail-on: deviates)". `fail-on: never` does the same, "(exit 0 by your fail-on: never)". `never` keeps exit 0, with one exception: a prove-class obligation that is not proven exits 1 with `policy.reason` `not_proven`, because a prove-class obligation is red unless proven and no run can set that aside. When the line leads with a refute-class disproof and the exit 1 comes from such an obligation, it ends "(exit 1: not proven — \<obligation>)". When the line leads with a disproof your `fail-on` leaves out and the exit 1 comes from a status you named, it ends "(exit 1 by your fail-on: \<status> — \<obligation>)". `allow_partial` is still accepted and changes nothing.

The first line of the job summary, the Action's first notice and its last log line carry the same words, in this order: the colour, the coverage, the bounds, then the finding.

- Coverage: "E of F \<units> observed; " for a selected scope, then "N of M machines read", with ", K refused" when a machine was refused.
- Bounds: "declared premises: n; version pins: \<pins or none>". An observed profile that declares nothing reads "declared premises: none (observed profile)". For a newly built `python-auth-declared-model` verdict, n counts the declared premises the checker confirms ("premise \<id> (\<kind>, declared): confirmed"). A stored verdict returned as stored retains its stored bound, so its first line can show an older premise count. Pins are named: "major 18", and the baseline set when the readings name one. Then "prove-class obligations: p (unproven: u)" when the profile has any, and "strict: no obligations marked" when you chose strict and the profile marks none.
- Red: "deviations: d" when any reading deviates, then the finding. A disproof reads "disproven: \<obligation> — \<object>, \<access path>, \<locator>", from the reading's first witness; with more than one, "disproven: k, first \<obligation> — …", where k counts the disproofs (a disproven requirements.md row none of whose obligations is already counted counts once), the first is in reading order, your own before platform-owned ones, and the free summary (QUICKSTART-API.md section 12) lists the same k under `disproven`, each marked `platform_owned`. A status you chose to stop on reads "stopped by your choice: \<status> — \<obligation>", never "disproven". A prove-class obligation whose input could not be read reads "not proven: \<obligation> — input not observed"; one read without a holds reads "not proven: \<obligation> — \<reason>; \<action>". Strict reads "unresolved by your choice (strict): \<obligation> — \<reason>; \<action>". With more than one, it reads "unresolved by your choice (strict): k, first \<obligation> — \<reason>; \<action>", where k counts them and the first is in reading order.
- Green, when at least one obligation on the machines read holds: "nothing disproven; established: h of N; [deviations: d; ][\<status>: k; …]not established: n — top action: \<action>". When none holds: "nothing could be established (0 of N obligations hold); [deviations: d; ][\<status>: k; …]not established: n — top action: \<action>". Every other reading class on the machines read, such as `vacuous`, is named with its count, in alphabetical order, so the parts account for every reading. N counts every obligation reading; h the holds on machines that were read. The top action is the action of the most frequent reason class, leaving out `representation` when any other class is present; a free quote follows it when the action is a paid run. One exception: when no reading was usable (`completion.usable_readings.count` 0) and every refused machine was refused for `representation` (the `status` of its `machines.refused` entry; the reason's `representation` prefix only for an entry stored without `status`), the other classes exist only because the collection is absent, so the line says "no machine's collection was read" just before "not established: n" and the top action is the representation action ("re-collect with the pinned collector"; a profile with no collector names its own input form), whether or not any not-established entry is of class `representation`. A check stored before this rule keeps, in its job summary and log, the first line the Action printed at the time; the record stores no first line, so its summary `headline` and `top_action` are rendered again from the stored record and follow the rule, and the two can differ for such a check. A deviation is counted on its own and is never a not-established obligation.
- Yellow: "yellow: could not look: \<reason>; \<action>", for example "yellow: could not look: the input could not be represented; re-collect with the pinned collector". Under it, the summary prints the input gate's reason and the file it names, when it names one.

For example: "green: 8 of 8 machines read; declared premises: none (observed profile); version pins: major 18; nothing disproven; established: 4 of 41; deviations: 2; vacuous: 14; not established: 21 — top action: state it in requirements.md". [FIRST-LINE-SAMPLES.md](/docs/first-line-samples) gives one line for each case, word for word, with the meaning sentence for each colour.

Under the first line, one sentence says what it means in plain words, from a fixed template per colour:

- green: "Nothing in your \<subject> contradicts what is known about safe \<family> setups. \<n> of \<N> checks could not be completed, mostly because \<the top reason>. That is a gap in what we could see; your \<subject> is unchanged by it." ("are" for a plural subject.) The subject and family are "database configuration" and "PostgreSQL" for the PostgreSQL profiles, "HTTP responses" and "HTTP" for the HTTP profile. For `python-auth-declared-model` the green sentence says what was read: "Every route in your Flask app is guarded as your declaration says." when AUTH-1 and AUTH-2 both hold, "Nothing read in the routes of your Flask app contradicts your declaration." otherwise, then the same counts with "your Flask app" as the subject.
- red: the consequence of what the first line names, then for a disproof "The finding is at \<locator> (\<object>, \<access path>)." when the reading's witness names a locator, or "The reading \<obligation> fails; the record carries no witness to point at." when it carries none. Until a profile states an obligation's consequence, it reads "Consequence not yet stated for \<obligation>."; nothing is invented. The auth catalogue states no consequences, so serving states AUTH-1's for `python-auth-declared-model`, bounded by what the checker reads: "Anyone who can reach your Flask app can call a route outside your declared public routes without signing in."
- yellow: "We could not read the collected files, so nothing below is a finding about your \<subject>. \<Action>."

"Why it matters" lists each disproven or deviating obligation once: its plain statement and its consequence, both from the profile (the observed PostgreSQL profile states both for every obligation); a profile that states no plain statement falls back to the reading's own statement.

Under a green line the summary first groups the obligations that could not be established by reason class, largest group first (ties by class name), one line per group: "\<n> rows not established — next step: \<action> (\<class>: \<reason>)" ("1 row" for one), so the first such line starts with the largest count and the one action that decides those rows. The verdict carries the same groups as `undecided_by_cause`: one `{class, rows, action, owner, ids}` per reason class of the `not_established` entries remaining after empty domains are left out, in that order; `action` is the action the group's entries share, for every class (a `representation` group of one entry carries that entry's own action); when they differ, the class's fixed action (the `action` column below; for `representation`, the profile's own pointer without a row's reason, and on `python-auth-declared-model` "review the repository or declaration input the reason names and resubmit"), else "collect the facts each row names" for `not_observed` entries that name different locators. A verdict built or re-decided by this release carries `undecided_by_cause` exactly when at least one `not_established` entry remains after empty domains are left out (a verdict stored before it, read back under its own fail-on, is returned as stored); a verdict with no not-established entry carries no `undecided_by_cause`, and its summary reads as before. The summary's group lines count the entries remaining after empty domains are left out; the profile's own limits (below) are grouped in the verdict field but listed apart in the summary. Then the summary names each obligation that could not be established, "\<obligation>: \<reason> — \<owner>: \<what it needs>". A `python-auth-declared-model` obligation the checker reads because no elevation key is declared (AUTH-5: nothing to elevate) is an empty domain, whether the checker reads it "not established (vacuous)" or, when requirements.md claims AUTH-5, "not established (missing_premise)": "\<obligation>: empty domain, nothing to check — \<the checker's reason>", with no owner and no action. Its verdict JSON `not_established` entry keeps its `status` and `reason_class` and carries `owner`, `needs` and `action` null; its JSON summary entry also carries `action_source` null. A requirement whose every matched obligation is such an empty domain reads the same words after its state, "R5 — "\<sentence>" — not established — empty domain, nothing to check — \<the checker's reason>", in the summary and in its annotation, and its JSON summary row's `next_step` is null. Such an empty domain is in no group line and no `undecided_by_cause` group: it has no next step to group. When more than five obligations share the same words after their id, the summary prints those words once, "Next step for \<n> obligations: \<reason> — \<owner>: \<what it needs>", with the n obligation ids on one line under it; five or fewer keep one line each. The JSON summary's per-row `next_step` and the Action's annotations are per obligation either way. The JSON summary (`GET /v1/checks/{id}/summary`) also carries `top_action` — `{text, reason_class, action_source, from_entries, rule}` — so the first line's "top action" can be checked from the reply: `text` is the action the green first line prints (unclipped; the line clips at 300 characters), `from_entries` the ids of the not-established entries the rule counted for it, and `rule` the rule in words (the action of the first entry of the most frequent reason class, leaving out `representation` when any other class is present, unless no usable reading was made and every refused machine was refused for representation; then `reason_class` is `representation`, and `from_entries` is empty and `action_source` `class_default` when no entry is of that class); it is null when no entry carries an action and on a red or yellow verdict, whose first line prints no top action. The summary's `not_established` entries each carry `action_source`, where the entry's `action` came from: `row` when a not-observed reading itself named what to collect (its gap, premise locator or raw/ path), `class_default` for every other reason class, whose action the class itself fixes rather than the reading (the `action` column below; the HTTP profile's own `needs_intent` wording; for `representation` on a profile with no collector, its own pointer), `locator_fallback` when a not-observed reading named no gap, locator or raw/ path and the action is the profile default `collect the facts <id> reads`; null for an entry with no action. The summary's entries carry `id`, `machine`, `status`, `reason_class`, `action`, `action_source`, `owner`, `needs` and `quote`; the full record (`GET /v1/checks/{id}`) keeps any further key of the stored entry, such as the HTTP profile's `fact`. Such an entry's line reads "a fact was not collected: no locator on the row; action from the profile default" instead of naming a locator that was never collected. The reasons, their owners and what they need:

| `reason_class` | Reason | `owner` | `needs` | `action` |
|---|---|---|---|---|
| `not_observed` | a fact was not collected: \<locator> | agent | collect \<locator> | collect \<locator> |
| `missing_method` | no method for this shape | symbolia | a method for this input shape | a method is a later profile version |
| `missing_baseline` | no pinned baseline for major \<n> | symbolia | a pinned baseline for major \<n> | pin the baseline (Symbolia) |
| `needs_intent` | depends on what the system is for | operator | a requirements.md sentence | state it in requirements.md |
| `representation` | the input could not be represented | agent | a faithful input: \<reason> | re-collect with the pinned collector |
| `not_observable_at_pin` | not observable at collector pin \<pin> | symbolia | a collector successor that observes it | collect with the collector successor (Symbolia) |

On `postgresql-observed-baseline` and `python-auth-declared-model` every entry also carries `because`: `{gate, premise, source}`, which input sent it there. `gate` is `machine` when the derive or the checker refused the entry's machine or did not read it (listed in `machines.refused`, refused by the derive result, or named by a refusal row); then `premise` is the refusal reason and `source` the raw file it names (null when the reason names none): with `raw/pg_hba.conf` absent and no `COLLECTION-SIDECAR.json` entry explaining it, every M2, M3 and M4 entry reads `{gate: "machine", premise: "representation: pg_hba.conf absent from raw", source: "raw/pg_hba.conf"}`; when the sidecar records it as not collected (`missing`, `unsupported` or `unreadable`) and `raw/catalog_snapshot.json#pg_hba_file_rules` is not readable, only M2 is refused (with the view readable and its rows without an error, the rules are read from it and M2 is not refused for the file; issue #2942), and on M3 and M4 only the rows that read pg_hba rules (TLS-2-OB, REP-1-OB) stay in `not_established` for it; their reason names the not-collected file and their `because` reads `{gate: "reading", premise: null, source: "raw/pg_hba.conf"}`. Otherwise `gate` is `reading`: the machine was read and this reading was not established. Premises the reading marks `role: "informational"` are not its reason and are skipped. `premise` is the first premise of the reading taken from the declaration (`source` `raw/declaration.json#<field>`, e.g. `raw/declaration.json#/intended_flows` for a `needs_intent` HBA-2), else the first premise not observed (`source` the raw file its gap names); with neither, `premise` is null and `source` is the raw file the reason names, or `raw/declaration.json` for a `needs_intent` entry (HBA-1-OB, whose only unobserved premise is informational, reads `{gate: "reading", premise: null, source: "raw/declaration.json"}`). A part nothing names is null. `because` is in the full record; it changes no status, count, policy or exit.

On `http-observed-baseline` no requirements.md sentence can hold yet, so a `needs_intent` entry never points at requirements.md: its action and need name the `raw/HTTP-INTENT.json` key that decides it, "declare redirects in raw/HTTP-INTENT.json beside the collected files (docs/customer/VERDICTS.md); requirements.md cannot state an HTTP requirement in this release" for HTTP-REDIRECT-POLICY, with `response_headers` for HTTP-RESPONSE-POLICY-ADEQUACY and `cookies` for HTTP-COOKIE-POLICY in place of `redirects` (the only three obligations that read `needs_intent` on that profile; the summary's group line for them reads "declare in raw/HTTP-INTENT.json the key each row names (docs/customer/VERDICTS.md); requirements.md cannot state an HTTP requirement in this release"), and the coverage phrase the report keeps as `first_line` then ends "; no HTTP requirement can hold in this release". Issue #2381: those rows decide when the bundle carries the optional `raw/HTTP-INTENT.json` you write beside the collector's files, for every selected endpoint (the Action sends an `HTTP-INTENT.json` placed beside `manifest.json` as that file): the seven standing flags and one or more of `redirects` (`allow`: `same_origin`, `same_site` or `any`; `forbid_secure_to_insecure`: true or false), `response_headers` (`require`: a list of lowercase header names: `x-content-type-options`, `referrer-policy` and the CSP and HSTS header names) and `cookies` (`require`: `httponly` and the HTTPS-only attribute of RFC 6265 section 4.1.2.5 booleans, where true requires the attribute and false or an absent key leaves it unrequired, `samesite` a list of `Lax`, `Strict`, `None`). HTTP-REDIRECT-POLICY fails a Location outside the allowed origin relation, or an https-to-http redirect when that is forbidden (no site relation is collected, so a cross-origin Location under `same_site` reads `not_observed`); HTTP-RESPONSE-POLICY-ADEQUACY fails an endpoint missing a required header (the HSTS header is required only on https, as RFC 6797 ignores it over http; a required header the collector withheld or did not read whole reads `not_observed`, unless another required header is absent); HTTP-COOKIE-POLICY fails a cookie lacking a required attribute (the HTTPS-only attribute only on https), a cookie attribute or `Set-Cookie` field the collector could not read reads `not_observed`, unless another cookie is observed lacking a required attribute, and an endpoint whose collected head sets no cookie reads `vacuous`. Each holds otherwise. A key the file does not carry, or no file, leaves its row `needs_intent`. When the file is sent the readings record its sha256 as `inputs.intent_sha256` (and in the causal proof's pins), and each row a declared key decides lists it as a `declared` premise (`artefact` `raw/HTTP-INTENT.json`, `locator` `/redirects`, `/response_headers` or `/cookies`, as the row's `intent_input` in the checker's PROFILE.json names it); with no file neither appears. A file of another shape refuses the check (`profile_refused`, cause code `INTENT-SCHEMA`) and is an `http` finding in lint. On that profile a `not_observed` entry also carries `fact`: `{file: "raw/HTTP-FACTS.json", endpoint_id, field}`, the collected fact the reading was derived from (`capture_status` for a head not collected, `facts.status_code` for a 4xx or 5xx answer). Lint refuses an endpoint only when the checker's coverage reader finds no usable observed premise; a 4xx or 5xx response can still supply readable framing facts.

An obligation the profile marks `observable_at_pin: false` reads `not_observable_at_pin` under every `fail-on`, not only strict: the next step is never one the pinned collector cannot take, so what the agent can collect (a `not_observed` entry) stands apart from what needs a collector successor. Strict adds one further thing: such an obligation with no reading at all is listed too, never silently skipped.

A vacuous reading means there was nothing to check in its domain: it is counted, never listed. The declared profile's missing premise reads `not_observed` and its not evaluated `representation`. `quote` is null until a profile names a priced action. Obligations of class `representation` and `missing_method` are the profile version's own limits: the summary lists them under "Unsupported by this profile version: n", apart from the rest.

The verdict also carries `colour`, `bounds` (`declared_premises`, `observed`, `version_pins`, `coverage`), `not_established`, `undecided_by_cause` (the not-established entries by reason class, largest first; section 9), `bias` (`default`, `declared`, `prove_total`, `prove_unproven`, `prove`, `security`), `why` (each disproven or deviating obligation with its statement and its `reason`, up to 300 characters of the reason text its row in `rows` carries, null when the row has no reason text), `consequences` (the consequence sentences the profile states) and `completion`: `usable_readings` {`count`, `of`}, `coverage_complete`, `policy_passes`, `unsupported` {`count`, `ids`} and `snapshot` {`collected_at`, `freshness_s`, `loaded_state`}. The summary answers the three questions in three short sentences under the meaning sentence: "Usable readings: u of n. Coverage complete: yes. Job stops: no (exit 0 by your fail-on)." (or "Job stops: yes (exit 1)"; "Job stops: no (exit 0: platform-owned)" when every disproof your `fail-on` names is platform-owned). When the collector's sidecar names them, the collection time and its age, and the loaded state, follow on the same line: "Collected at \<time>, \<s> s before this summary. Loaded state: \<state>." The envelope keeps them in `completion.snapshot`.

## 10. Your requirements

When the check carried your `requirements.md`, the verdict also carries `requirements`: one row per requirement line, with its `id`, its `sentence` as you wrote it, and one of three states. **holds**: the methods it matches hold within the bounds the first line states. **not_established**: nothing establishes it yet; `reason_class` says why and `establishing_action` what would establish it. **disproven**: a reading with a witness contradicts it. A row that matches a prove-class obligation that is not proven keeps the state `not_established` with `display_state` `not_proven` and `blocking` true. A requirement holds only when the check ran to its end: a check the input gate stopped, or one whose effort is not accounted, never yields holds.

On `postgresql-declared-model` and `mysql-declared-model`, the verdict also carries `declaration`: `confirm_leaves`, the JSON pointers, in lexicographic pointer order, of the declaration's leaves that still await confirmation (its `declaration_state` markers, read from `raw/declaration.json`, or for MySQL from `raw/submission.json`, where the collector copies them; [DECLARATIONS.md](/docs/declarations#drafted-declarations)), and `confirmed`, true when none does. While `confirm_leaves` is not empty the first line ends "; provisional: N leaves await confirmation", in those words for every N, one included: every reading on such a leaf is provisional, not final. Each obligation row, and each requirement row through its matched obligations, lists `depends_on_leaves`, the pointers of the declaration leaves its witness depends on, when the checker's reading names them; on `postgresql-declared-model` each M5 (RLS) obligation row names them, when your declaration carries any: `/protected_tables/<i>` for the first declared table the snapshot has (the one M5 models), then whichever of `/declared_predicate`, `/required_policies`, `/access_paths_complete`, `/rls_exempt_logins`, `/view_exemptions` and `/major` your declaration carries, the fields M5 is built from. A row without the field is one no leaves are named for, not one that depends on none; today that is every row outside M5's obligation rows, MySQL's included.

The first line adds "requirements: h hold, n not established", with ", d disproven" when any is, after the not-established count (before the finding on a red line). Under the not-established list, the summary lists every requirement with its sentence as you wrote it, "R3 — "\<sentence>" — not established — missing method — \<action>": the sentence is cut at 160 characters with …, the line names the locator after the reason class when the row's evidence carries one (for a disproof, where the counterexample is), for `python-auth-declared-model` it names the reason class only when it reads differently from the state (a line that holds reads "— holds — \<action>", a disproof "— disproven — \<locator> — \<action>"; other profiles keep "— holds — holds — \<action>"), and it ends with the quote's USD range when the row carries a free quote. The meaning sentence ends "\<u> of \<N> requirements in requirements.md could be read." A disproven requirement is red, as its disproof is; one that reads not proven exits 1 under every `fail-on`. Each requirement that does not hold is also one annotation in the same words. A check sent without requirements carries no `requirements` field, and every other field is as before. A requirement checked with `http-observed-baseline` cannot hold today: the HTTP check does not yet carry a verified method and path for each endpoint, so a matched HTTP requirement reads not established with the reason class `missing_identity_binding`. `mode: local` does not read `requirements.md`: requirements are checked in `mode: api` only, and a local run that finds one says so in one notice.

Exit 2 is bad input, and a failure envelope gives its outcome's own exit, 3 for every outcome that has none. A failure of the checker itself is the failure envelope `checker_error`, under every policy. For a newly run API check, the envelope's `charged` states the server's decision (section 1). After the checker has started, `checker_error`, `checker_timeout`, `checker_cpu_limit` and `checker_memory_limit` are charged unless the answer states an explicit no-charge decision. The checker's own internal error, a request refused before admission, a check cut short by a server restart, and a malformed bundle refused by the free pipeline for `python-auth-declared-model` are not charged. Local runs charge nothing; reading a stored answer or replaying an idempotent request does not charge again. A historical stored answer without the field leaves that charge unknown. A request still counts against your rate limits.

A result is returned whenever one can be produced. When the checker's output for one machine is malformed, that machine is refused and named under "Machines not read" with its reason, `malformed_output: ...` (or `server_fault: ...` when the server could not write that machine's readings), and the other machines keep their readings; a refused machine never contributes a verdict, so the policy reads it as any refused machine. When no result can be produced, the typed failure says what was malformed in `detail.malformation`: a kind from a fixed list, the file, the line, the field and the expected form, never the text of your input. Where the server holds the path of the key it stopped on, the malformation also carries `pointer`, the RFC 6901 JSON pointer of that key in your request body (such as `/files/raw~1submission.json`, `/extra` or `/scope_binding/endpoint_tokens/endpoint-003`), and the reason names that key or, where the key is a selected id (customer text), points at `detail.malformation.pointer`; a key outside the alphabet A-Z, a-z, 0-9, `_`, `.`, `-` and `/` is never quoted, and the pointer then stops at its nearest such parent (`/files`), as it does for a file name with an empty, `.` or `..` segment or one the malformation's `file` would not quote. A rule or setting the collector withheld as a marker line is listed in `not_observed_lines` and in the summary as `not observed: <file> line N (<reason>)`; the check reads it as not observed.

`fail-on` takes, for each profile, the statuses it can emit except holds and vacuous, the group word `not_collected`, or `never` alone; the quickstarts list them.

## 11. The lint record

`POST /v1/lint` and the Action's `mode: lint` answer with a lint record, schema `assure.serve.lint/v1`. It is not a verdict: no check ran and nothing was charged.

```json
{"schema": "assure.serve.lint/v1", "ok": false, "profile": "postgresql-declared-model",
 "findings": [{"stage": "files", "outcome": "bad_input", "severity": "refusal", "charged": false, "kind": "file_missing",
               "text": "Missing required file raw/declaration.json.", "file": "raw/declaration.json"}],
 "requirements": {"rows": 3, "problems": 1, "row_details": [{"line": 1, "id": "R1", "state": "matched", "reason_class": null, "obligations": ["PRIV-1"]},
                           {"line": 2, "id": "R2", "state": "free_text", "reason_class": "missing_method"},
                           {"line": 3, "id": "R3", "state": "problem", "reason_class": "not_understood"}],
                  "counts": {"rows": 3, "problems": 1, "matched": 1, "free_text": 1}},
 "stages": [{"name": "body", "state": "ran"}, {"name": "query", "state": "ran"}, {"name": "profile", "state": "ran"},
            {"name": "fail_on", "state": "ran"}, {"name": "scope", "state": "ran"}, {"name": "files", "state": "ran"},
            {"name": "collector", "state": "ran"}, {"name": "requirements", "state": "ran"},
            {"name": "http", "state": "skipped"}],
 "would_run": false, "would_charge": false,
 "stages_run": ["body", "query", "profile", "fail_on", "scope", "files", "collector", "requirements"],
 "stages_skipped": ["http"],
 "not_checked": ["sidecar_digests", "credit", "day_window", "queue", "engine_pin", "checker_run"],
 "inputs_judged": null,
 "build": {"commit": "<40 hex>", "release_sha256": "<64 hex>", "checkers_sha256": "<64 hex>"}}
```

The record also carries the seven flags and `build` `{commit, release_sha256, checkers_sha256}`, the same three keys as a verdict's `engine` block (section 1), naming the engine that answered. Every new refusal envelope the API builds and the `GET /v1/profiles` answer carry the same top-level `build` block; a refusal stored before the block existed is re-served as the bytes written then, without one; the `checker_error` that refuses a development verdict carries the server's own build identity, never the refused development block, so it has no `kind` or `tree_sha256`; and a failed engine-pin verification makes the block exactly `{commit: null, release_sha256: null, checkers_sha256: null}`. A client comparing two answers should compare `build.commit` first: a difference means a different engine answered, so a changed answer may be a deploy, not a changed upload. When served by `POST /v1/lint`, the record also carries a top-level `request_id` equal to the `X-Request-Id` response header (issue #1101): a lint has no `check_id`, so this is the citation for it. `ok` is true exactly when `findings` is empty. `profile` is the profile id, or null when none could be read. `requirements` keeps the two integer counts `rows` and `problems` at their original keys and adds `row_details`, the list saying how each row was read; the block is null when no profile or no requirements text could be read. Each `row_details` entry carries its `line`, its `id` and one of three states: `matched`, the sentence is one of the obligation statements the checker publishes, and `obligations` lists their ids; `free_text`, the row is understood but no method reads it, and `reason_class` says why (`missing_method` for a plain sentence; `checker_contract` when the checker's statements could not be loaded, are not a list, or the matcher refuses them, as a paid check reads the same rows); `problem`, the row is one of the three requirement findings below and `reason_class` names which. `counts` keeps the four totals beside the rows: `rows`, `problems`, `matched` and `free_text`. When the checker publishes no obligation statements, the block also carries `matching`, a note beginning `none:` that says so; when its statements could not be loaded or read, `matching` begins `none:` and names `checker_contract`. In both cases every row that is not a `problem` reads `free_text` (a row the parser cannot read, a repeated id or a tag for another family is still a `problem`), and metadata that could not be loaded is not a finding: the block is still given. Issue #378 adds `row_details` and `counts` beside the original integer `{rows, problems}` keys. The schema id is unchanged, and a reader of the old shape continues to read those integers at the same keys; `counts.rows` and `counts.problems` repeat them.

`would_run` and `would_charge` describe lint's prediction for a check of the same body; they do not record a charge for the lint request. `would_run` is true when no finding is a refusal. `would_charge` is true when the check would run or any refusal finding predicts a charge, and false otherwise. Each finding carries `severity` and `charged`: `severity` is `refusal` when its `outcome` is a typed failure that would stop the check, and `notice` when it is a coverage fact the check can still run with. Its `charged` uses the shared outcome table and any explicit refusal detail, so a `profile_refused` before admission or from the free Python pipeline is not automatically charged; notices are not charged. A surface that sorts findings reads `severity`, not whether `outcome` is null. Credit, the day window, queue capacity, the engine pin and the checker run remain unchecked by lint, as `not_checked` lists; the completed check's envelope states the actual charge.

`ok: true` is not a clean bill; it says that the stages that ran found nothing. Four keys say what that covered. `stages` lists the nine stages, in their order, one row `{name, state}` each, and `state` is one of three words. `ran`: the stage inspected this upload, whether it found a fault or not (a clean record has every row `ran` or `not_applicable`, so an empty `findings` means those stages looked and found nothing). `skipped`: an earlier stage failed, so this one could not run (an unknown profile skips everything after `profile`; a missing required file skips the readers and so `http`; a body that cannot be read skips everything but `query`, which does not depend on it). `not_applicable`: the profile has no input of that kind, so there was nothing to inspect (`http` on a PostgreSQL upload; on `http-observed-baseline` it is `ran`). `stages_run` and `stages_skipped` partition the same nine names the same way: a stage is in `stages_skipped` when its state is `skipped`, and in `stages_run` otherwise, so `stages_run` holds the `ran` and the `not_applicable` rows. `not_checked` names the admission checks of `POST /v1/checks` that this lint did not perform: `sidecar_digests` (each file's bytes against the collector sidecar's digest for it, so on a profile whose lint does not compare them an edited bundle passes lint and is refused by the check, as in the `postgresql-declared-model` example above; for `http-observed-baseline` the checker's reader does compare the response heads and facts file with the sidecar, so an edited one is an `http` finding naming `ARTEFACT-DIGEST-MISMATCH` (a head) or `FACTS-DIGEST-MISMATCH` (the facts file) and this name is dropped once the `http` stage ran), `credit`, `day_window`, `queue`, `engine_pin` and `checker_run`. `inputs_judged` is `{required: [paths], optional_present: [names], optional_absent: [paths or patterns]}`: the profile inputs the checker's own readers judged, the optional inputs you sent, and the optional inputs you did not send, so a record whose `optional_absent` names `raw/COLLECTION-SIDECAR.json` was judged without it; it is null when the readers did not run (a stage before them failed).

After file names, sizes and scope pass, lint uses the selected checker's own input readers for `submission.json`, `HTTP-FACTS.json`, `declaration.json` and `catalog_snapshot.json`. An unreadable input is a `files` finding naming the file. For `postgresql-observed-baseline` lint also runs the derive's major rule, read-only: an upload from which the major version cannot be observed (no `raw/COLLECTION-SIDECAR.json` `server.server_version_num`, and not both `raw/command_line.json` and a `catalog_snapshot.json` `roles` roster that agree), whose readings disagree, or whose major is outside 14..18 is a `files` finding with outcome `profile_refused`, kind `derive_refused`, `file` `raw/COLLECTION-SIDECAR.json` and `field` the derive's kind (`major_not_observed`, `major_disagreement` or `major_out_of_range`), the same `detail.malformation` the paid check refuses with. When the major is observed, a `catalog_snapshot.json` holding none of the snapshot sections the collector writes (`collection`, `roles`, ...), such as `{}`, is a `files` finding with outcome `profile_refused`, kind `derive_refused` and `file` `raw/catalog_snapshot.json`, and `POST /v1/checks` refuses that upload the same way before admission, uncharged, instead of a `no_verdict` run that read nothing. HTTP facts are also checked against their sidecar, response heads and selected scope: facts that are not JSON, lack the seven flags or have no endpoints list, and a response head whose digest is not the sidecar's `sanitized_sha256` for it, are each an `http` finding with outcome `profile_refused` naming the file the paid check would refuse; when the checker refused with one of the HTTP derive codes QUICKSTART-API.md section 9 tabulates, the finding's `file` is the file the checker located the fault in (`raw/COLLECTION-SIDECAR.json` for a sidecar fault) and its `text` names the code and gives that code's next step for that file, the same one the paid check's `action` gives when the checker located the fault in a file (a `scope` that is not the collected endpoint set says to send the tokens of the collection run; an edited file says to send exactly the files the collector wrote); a captured endpoint with no usable response evidence is an `http` finding with kind `endpoint_no_usable_case`. Lint inspects these inputs without starting a check or writing a check record. An accepted input can still produce a disproof or a bounded gap when checked.

When a file name is outside the profile's input list, the finding carries `accepted: {required: [paths], optional: [paths or patterns]}` and names those inputs in its text. `POST /v1/checks` gives the same wording and the same lists in the refusal's `detail.accepted`; these are the inputs published by `GET /v1/profiles`.

Each finding has `stage`, `outcome`, `severity`, `charged`, `kind` and `text` (one sentence saying what to change), and `file`, `line`, `column`, `field`, `pointer` (the refusal's `detail.malformation.pointer`, section 10) and `endpoint_id` when they apply. `file` is a `raw/` path, the name you sent for a file outside `raw/` (a `file_name` finding on the bundle root zipped instead of `raw/`, when the name is of the same grammar: segments of A-Z, a-z, 0-9, `_`, `.` and `-`; its text adds that names must start with `raw/`, see `GET /v1/profiles` inputs), or `requirements.md`: that name is the lint's fixed label for the `requirements` text of the body, whatever file you read it from (the Action sends its `requirements` input's file, else the `requirements.md` at the repository root; an API caller sends the text), so a finding about that text carries `requirements.md`, and carries `line` when the fault is on one line (a row the parser cannot read, a line over the byte bound) and no `line` when it is about the whole text (not text, not UTF-8, over the byte or line bound). A file you send named `requirements.md` is not that text: its `file_name` finding leaves `file` out, and its text says to send the text as the body's `requirements` field. A finding never carries any other name as `file` (a name outside that grammar, such as one with a control character or a `..` segment, is left out); when no name is known, `file` is left out. A `files` finding on `raw/declaration.json` that the `postgresql-declared-model` reader refuses says where: for UTF-8 bytes that are not JSON it carries `line` and `column` and its text ends with the JSON decoder's own message at that position ("(line 1 column 16: Expecting property name enclosed in double quotes)" for `{"schema":"x", `); for a JSON object the declaration schema faults it carries `field` (the validator's path, `/major` as `major`) and its text ends with the validator's whole message ("(major: Expected integer)"); when the derive refused on `major`, that field is the one named even if the validator lists another fault first, so one correction is one round trip; only a field the schema names is ever quoted, never a key or a value of yours. The check's `profile_refused` reason for a file that is not JSON keeps the derive's text and adds the same position; for a schema fault the check's reason is the derive's own and names no field. A registered stack without a runner leg on this build instead has kind `stack_not_wired`, `file` `raw/declaration.json` and `field` `stack`, with the missing runner leg named. That lint finding has no `pointer`; the paid refusal carries `detail.pointer` `/stack`. `go/gin` names `assure/checkers/identity/adapters/go_gin` and `typescript/express` names `assure/checkers/identity/adapters/typescript_express`. For other schema faults, a `files` finding on `raw/declaration.json` that the `python-auth-declared-model` reader refuses (issue #1959) has outcome `profile_refused` and kind `declaration_invalid`, the same `detail.kind` the paid check refuses that upload with, and its text names the schema keyword. It carries `pointer`, the JSON pointer of the checker's first schema fault and the same `detail.pointer` as the paid check's (`/stack` for a key the schema does not have, `/guards` for a `guards` that is not a list), when that pointer is of the closed grammar of section 10 (segments of A-Z, a-z, 0-9, `_`, `.`, `-` and `~1`); a pointer outside it, one that would quote a key of yours such as `my key!`, is left out of the finding and its text. A declaration that is not a JSON object keeps kind `derive_refused` with `field` `raw_unreadable`. The stages run in this order: `body`, `query`, `profile`, `fail_on`, `scope`, `files`, `collector`, `requirements`, `http`; an unknown profile is listed first, because no other fix can matter until the profile exists. A stage that cannot run because an earlier one failed is left out. Each stage up to `collector` reports its first problem, except that the `body` stage also gives one finding with outcome `unknown_field` (kind `body_shape`) for each of the first 16, in name order, of the top-level keys of a JSON object body that the server does not read (issue #2126; past 16, one more `unknown_field` finding, without `field` or `pointer`, says how many more keys were not named), with `field` and `pointer` naming the key when it is of the closed grammar of section 10 (a key outside it is counted and never quoted) and a `text` naming the lint schema, the engine commit that answered (`engine unknown` while the engine pin fails to verify and `/healthz` answers 503 without `engine_commit`) and the fields the server reads (`profile`, `files`, `scope`, `scope_binding`, `accepted_scope_ref`, `requirements`, `repository`, `commit`, `provenance`, `compile_requirements`), such as "the server (lint schema assure.serve.lint/v1, engine 1111111111111111111111111111111111111111) does not read scope_version; fields it reads: profile, files, ..."; when the body lacks no required key these findings take the place of the `bad_input` body-shape finding. `ok` stays false for them, because `POST /v1/checks` refuses that body `bad_input` (send only the fields the server reads, or wait for a server that reads the field). `requirements` gives one finding for each row that is not understood (`not_understood`), repeats an id (`duplicate_id`) or carries a tag for another profile family (`scope_mismatch`), with its `line`; a row that is understood and states no method is a normal answer and not a finding. `http` applies to `http-observed-baseline`: findings name endpoints with no usable observed premise, using the checker's own coverage reader. Each carries `field`, the relevant endpoint field of `raw/HTTP-FACTS.json`. Malformed headers do not discard independent usable framing or cookie witnesses, and a 4xx or 5xx status alone is not a finding.
