From the Assure repository at 262612b1426d, rendered as written. Source · Markdown

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.

FieldMeaning
schemaAlways assure.serve.verdict/v1.
check_idA random 32-character hex id for this check.
profileThe profile that ran, for example postgresql-observed-baseline.
profile_versionThe version of that profile's checker entry.
enginecommit, 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.
inputfiles: each file's path, bytes and sha256. bundle_sha256: one digest over that list.
outcomeverdict 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.
chargedWhether 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.
readingsThe checker's full output, unchanged. This is the proof tree (section 3).
countsThe 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_refusedcount: 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.
a0Null, or the reason the input failed its integrity gate (section 7).
machinesread: 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).
rowsOne 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.
policyfail_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, completionThe 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_factsAn 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_notesLimits that bound every reading in this check. See SCOPE.md.
warningsA 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 flagsFixed 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.

fieldvaries by
schemabuild
execution_authorizedbuild
hardware_authorizedbuild
industrial_release_authorizedbuild
release_allowedbuild
physical_validationbuild
simulationbuild
self_approvedbuild
check_idrun
profileinput
check_pathrun
enginebuild
headlinebuild
coverage_linerun
warningsinput
scope_notesinput
boundsinput
coverageinput
machines_refusedinput
machinesinput
outcomeinput
colourinput
policyinput
disproveninput
countsinput
requirements_countinput
requirementsinput
top_actioninput
not_establishedinput
shadowrun
varies_bybuild

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.

StatusVerdictMeaningHow it is written
holdsyesThe observed system meets the obligation over a non-empty observed domain."holds", or "holds in database <db>". It is bounded by its qualifiers.
failsyesAn 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
deviatesyesA recommendation is not followed. authority says whose recommendation."deviates from vendor guidance <row id>" or "deviates from the Assure baseline <row id>"
vacuousyesThe observed domain is empty. There is nothing to check."vacuous: observed empty domain", sometimes with " in database <db>". It always carries domain_locator.
needs_intentnoThe 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_observednoA fact the reading needs was not collected or could not be read."not observed: <what> (<locator>)"
missing_baselinenoA baseline row the reading needs is absent or not documented for this major."missing baseline: <row id> not pinned for major <n>"
missing_methodnoThe checker has no method for this input shape."missing method: <gap id>"
representationnoThe 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.

FieldMeaning
statusOne of the nine statuses.
status_textThe short text from the table above. Show it as given.
reasonThe full reason. For not observed it names what is missing and where it was looked for.
authorityPresent on deviates: vendor or assure.
witnessesA list. For fails, deviates and needs intent: every example that shows it. Empty otherwise.
qualifiersStrings that bound the reading, such as the database it covers.
observationsObserved facts that bear on the reading.
premisesEvery input the reading used, each with its source (section 4).
domain_locatorFor vacuous: where the empty domain was looked for. Null otherwise.
statementThe 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:

SourceMeaning
observedRead from your system. It names the file (artefact), its artefact_sha256 and a locator inside it.
baselineA pinned public baseline row. It names the row, its digest and its authority_class: vendor_documentation or assure_baseline.
declaredA value you supplied in raw/declaration.json. It can only tighten a baseline.
intent_requiredAn input only you can give. The reading is "needs intent: out of scope for Assure".
not_observedA 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.

IdMachineClassScopeStatement
PRIV-1M1intent-bounddbDeclared ordinary-ACL privilege queries are answered as expected.
PRIV-2-OBM1intent-freeclNo login other than an observed superuser or the collection role can become superuser or reach the server-file/program roles.
PRIV-3M1intent-bounddbRoles declared disjoint share no ordinary ACL privilege.
PRIV-4M1intent-bounddbNo sensitive object has a PUBLIC ACL entry.
PRIV-5M1intent-bounddbDefault ACLs equal the expected ACLs.
PRIV-6M1intent-bounddbDeclared revokes leave no dependent grant.
GRANT-OPTION-POLICYM1intent-bounddbNo sensitive-object grant option is held outside the administrators and the declared table_owners (issue #2570).
HBA-1-OBM2intent-freeclNo non-host-local TCP tuple is decided by trust, or by password on a non-hostssl record.
HBA-2M2intent-boundclEvery intended connection is accepted by its expected method and record.
HBA-3-OBM2intent-freeclNo configured pg_hba record is fully shadowed over the configured tuple domain.
HBA-4M2intent-boundclPhysical replication is admitted only for the operator's roles and standby addresses.
HBA-5-OBM2intent-freeclEvery record admitting a BL-ASSURE-TLS-PRIV-1 login from a non-host-local address is hostssl.
HBA-6M2intent-freeclThe server's own pg_hba parse has no error rows and loaded rules equal the file (not observable in this version).
TLS-1-OBM3intent-freeclEvery server-originated channel uses verify-full.
TLS-2-OBM3intent-freeclEvery non-host-local record admitting a privileged login is hostssl and ssl is on.
TLS-3M3intent-boundclRecords required to use client certificates use cert or verify-full with ssl_ca_file set.
TLS-4-OBM3intent-freeclWith ssl on, ssl_min_protocol_version is at or above PG-BL-TLS-MIN-1.
TLS-5-OBM3intent-freeclWith ssl on, the key file is 0600, or 0640 root-owned (not observed while the mode is not collected).
REP-1-OBM4intent-freeclEvery admissible physical replication path uses non-trust auth off-host, an explicit role and a non-wide address off-host.
REP-2-OBM4intent-freeclEvery physical slot has a matching standby consumer with bounded retention.
REP-3-CONFIGM4intent-boundclRequired no-loss durability selects a reachable synchronous standby.
REP-3M4intent-boundclNo committed transaction is lost on primary failure, when required.
RLS-1-OBM5intent-freedbNo protected relation is read or written unfiltered by a non-superuser, non-BYPASSRLS, non-owner path.
RLS-2M5intent-bounddbRequired policies exist and imply the operator's tenant predicate.
RLS-3M5intent-bounddbINSERT/UPDATE admission implies the operator's new-row predicate.
RLS-4-OBM5intent-freedbOwner-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-OBM5intent-freedbEach table with row-level protection on has a SELECT policy.
SD-1-OBM6intent-freedbEvery privileged SECURITY DEFINER function sets its own search_path, pg_temp last, no PUBLIC-writable schema.
SD-2-OBM6intent-freedbPrivileged SECURITY DEFINER functions with PUBLIC EXECUTE are listed.
SD-3-OBM6intent-freedbNo schema on a login's session search_path is CREATE-able by PUBLIC.
SD-4-OBM6intent-freedbThe public schema has no PUBLIC CREATE.
SU-1M7intent-boundclObserved superusers are exactly the operator's administrators.
SU-2-OBM7intent-freeclTransitive memberships in the server-file/program roles are listed.
SU-3-OBM7intent-freeclTransitive memberships in pg_read_all_data / pg_write_all_data are listed.
SU-4-OBM7intent-freeclEvery membership in the remaining predefined roles is a vendor-default edge.
LR-1M8intent-bounddbA publication cannot be read by a replication login other than the exclusive reader.
LR-1-SCOPEM8intent-bounddbPublished tables stay within the intended tables.
LR-2-OBM8intent-freedbrun_as_owner is false unless each target owner equals the subscription owner.
LR-2-APPLY-OBM8intent-freedbEach subscription target can be applied as its owner.
LR-3-OBM8intent-freeclEach subscription uses verify-full, password_required true, a non-superuser owner.
LR-4-OBM8intent-freedbPublisher 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.

IdAffected majors (first fixed)Precondition
CVE-2018-10589.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-734812 (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-245411 (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-3941815 (15.4)A relation has row-level security on (relrowsecurity).
CVE-2023-587011 (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-098512 (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-1097612 (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-871313 (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-1466614 (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-1467216 (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-1467314 (14.24), 15 (15.19), 16 (16.15), 18 (18.6)The amcheck extension is installed (17 is not affected).
CVE-2026-1468117 (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-647114 (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-1467618 (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:

Orderpolicy.reasonpolicy.exitColourWhen
1never0as the readings givefail-on is never. A prove-class obligation that is not proven still exits 1 (not_proven, row 5).
2bad_input, unauthenticated2redThe request or input was refused (bad_input), or the API key is missing, unknown or revoked (unauthenticated): a failure envelope, never a verdict.
3could_not_look3yellowNothing 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.
4disproven1redSomething 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).
5not_proven1redA 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.
6unresolved_strict1redUnder 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.
7stopped_by_choice1redA 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.
8platform_owned0redEvery 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)".
9nothing_disproven0greenOtherwise, 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 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_classReasonownerneedsaction
not_observeda fact was not collected: <locator>agentcollect <locator>collect <locator>
missing_methodno method for this shapesymboliaa method for this input shapea method is a later profile version
missing_baselineno pinned baseline for major <n>symboliaa pinned baseline for major <n>pin the baseline (Symbolia)
needs_intentdepends on what the system is foroperatora requirements.md sentencestate it in requirements.md
representationthe input could not be representedagenta faithful input: <reason>re-collect with the pinned collector
not_observable_at_pinnot observable at collector pin <pin>symboliaa collector successor that observes itcollect 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), 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.

{"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.