# Diagnostics

Quickback compiles your authorization model into a single normalized **policy
IR** — relations, permissions, arrows, scopes, grants, and per-resource
firewalls — and then runs a set of static checks over it. The analyzer never
changes what's emitted; it reports problems it can prove from the policy
graph: a permission that can never be satisfied, a scoped role no grant
reaches, a resource that hands a scope role more than its profile allows, a
negation that can silently pass.

## The console channel

Diagnostics render to the existing `[quickback:authz]` / `[quickback:authn]`
console channel — the same prefix the compiler has always used for security
warnings, so any CI grep you already key on keeps working. Each line carries
a stable, greppable **code**:

```
[quickback:authz] AUTHZ_EMPTY_PERMISSION: permission "event:locked" can never be satisfied — it requires organizerOf AND its negation simultaneously.
  at resource=sessions authz.permissions["event:locked"]
  hint: Remove the contradictory arm, or split into separate permissions.
  override: authz: { analyzer: { allow: [{ check: 'AUTHZ_EMPTY_PERMISSION', target: 'event:locked', reason: '<why this is acceptable>' }] } }
```

The first line stays greppable (`[quickback:<channel>] CODE: message`); the
source location, fix hint, and — for promoted errors — the exact override
recipe render on continuation lines.

## Errors vs warnings (v0.47+)

The checks split into two tiers:

- **Promoted (compile errors).** A trip is a *definite misconfiguration* —
  never a legitimate config. These fail the build by default. Each one can be
  explicitly accepted per finding via
  [`authz.analyzer.allow`](#the-override-surface).
- **Heuristic (warnings) and reports (info).** Advisory findings where a
  legitimate config could trip the detector. These print and the compile
  continues. Set **`QUICKBACK_AUTHZ_STRICT=true`** to promote these warnings
  to errors as well (overridden findings stay warnings even under strict —
  an override is an explicit, reasoned acceptance).

The analyzer remains compile-time only — zero runtime blast radius. A defect
in the analyzer *itself* is caught and downgraded to a single warning, so an
analyzer bug can never take down an otherwise-valid compile. Only real
findings fail builds.

## Authz codes

These read the permission graph projected from the policy IR and run on every
compile.

| Code | Tier | What it catches |
|---|---|---|
| `AUTHZ_EMPTY_PERMISSION` | **error** | A permission that can never be satisfied — `allOf(a, not(a))`, a fact and its exact negation at the same AND-level. The detector is conservative (exact-leaf contradictions only), so a trip is structurally unsatisfiable. |
| `AUTHZ_CAPABILITY_CEILING` | **error** | A resource's firewall grants a `scope:<kind>:<role>` role read access **beyond its capability profile** — either the role has no profile at all, or the profile doesn't list this resource. The M0.2 pillar guarantee. |
| `AUTHZ_SHADOWED_ARM` | warn | An `anyOf` arm subsumed by a broader arm: a leaf `X` and an `allOf(X, …)` in the same union — the narrower arm can never widen the result. |
| `AUTHZ_UNREACHABLE_ROLE` | warn | A scoped role declared under `authz.scopes` with **no grant profile** — it can authorize no read, action, or channel. Dead config, deny-safe. |
| `AUTHZ_UNREACHABLE_VIEW` | warn | A grant's `read` names a `resource:view` whose resource has no firewall — no role can reach that view. (A reachability proxy — heuristic by design.) |
| `AUTHZ_NOT_OVER_RESOURCE` | warn | A `not` whose operand resolves to a **resource (SQL) site** — `NOT EXISTS` over a nullable join can silently pass (the null-trap). The site resolution is conservative, so this stays advisory. |
| `AUTHZ_PUSHDOWN_COST` | info | Per-permission report: how many SQL subqueries the permission lowers to, plus the max recursive-CTE depth. Advisory, never fails a build. |

A few codes are reserved in the diagnostic enum for the milestones that emit
them and don't fire on a shipped config today: `AUTHZ_UNBOUNDED_RECURSION`
(an unbounded recursive arrow with no claim path — currently a hard compile
error raised at config validation, before the analyzer runs),
`AUTHZ_UNENFORCEABLE_FACT`, and `AUTHZ_CAVEAT_SITE_MISMATCH` (M3 caveats).

### The capability ceiling

The check that backs [scope grant profiles](/define/scopes): a
scoped role is deny-by-default and may touch only what its `grants` profile
lists. The analyzer walks every resource firewall, follows each
`{ permission }` arm into the permission graph to find the scope roles it
references, and **fails the build** if a resource grants a scope role access
the profile doesn't cover. This is what makes the grant profile the single
auditable manifest of what an external principal can see.

## Authn codes

Five **structural authentication** checks — proving properties over the
generated route + credential surface rather than the authz policy. All are
promoted (compile errors):

| Code | Tier | What it catches |
|---|---|---|
| `AUTHN_ROUTE_COVERAGE` | **error** | A route that reaches data with an **unclassifiable** auth gate and isn't declared `PUBLIC` — the signup / flag-gap bug class (data delivered to an unauthenticated ctx). Fires only on a gate the compiler cannot classify; zero-FP by construction. |
| `AUTHN_SURFACE_INVARIANT` | **error** | A surface contract breach: an `/mcp` or agent route not gated bearer-only, or an `INTERNAL`-gated route mounted on an HTTP-reachable surface. |
| `AUTHN_SECRET_CONSISTENCY` | **error** | A credential's mint site and verify site don't share key / issuer / audience — every minted token would fail verification (the ws-ticket "401 on every upgrade" trap). |
| `AUTHN_CREDENTIAL_DELIVERY` | **error** | A fire-and-forget credential send (the OTP-email shape) not anchored to `waitUntil` / `await` — the Worker may drop the promise and never deliver (the OTP-500 bug class). |
| `AUTHN_CLAIM_PROVENANCE` | **error** | An identity `ctx` field written from **request input** rather than a credential verifier — client-asserted identity, which is forbidden. Unprovable provenance also fails (fail closed). |

These check definitions are **wired and fully unit-tested**, but their inputs
— the per-route and per-credential metadata (`RouteNode` / `MintSite` /
`VerifySite` / `ClaimWrite`) — are not yet emitted by the route, middleware,
and credential generators. Until those generators emit that metadata the
analyzer runs the authn checks against **empty** route/credential graphs, so
they are effectively no-ops on a real compile. The severity contract is
locked in now: they fail builds the moment they can fire. (The emitted route
surface is separately guarded today by the IR's unbound-route invariant — a
hard error that predates this promotion and owns that surface; one subject,
one error.)

## The override surface

Every promoted finding can be explicitly accepted — per finding, with a
required reason:

```ts
authz: {
  analyzer: {
    allow: [
      {
        check: 'AUTHZ_CAPABILITY_CEILING',
        target: 'sessions/scope:event:organizer',
        reason: 'organizer grant profile lands with the v2 rollout (TICKET-123)',
      },
    ],
  },
}
```

An entry downgrades the exact `(check, target)` finding back to a warning
that cites the reason. The rendered error prints the exact recipe — copy the
`check` and `target` from it. Target formats:

| Check | `target` format |
|---|---|
| `AUTHZ_EMPTY_PERMISSION` | the permission name — `event:locked` |
| `AUTHZ_CAPABILITY_CEILING` | `<resource>/scope:<kind>:<role>` — `sessions/scope:event:organizer` |
| `AUTHN_ROUTE_COVERAGE`, `AUTHN_SURFACE_INVARIANT` | `<METHOD> <path>` — `GET /mcp` |
| `AUTHN_SECRET_CONSISTENCY`, `AUTHN_CREDENTIAL_DELIVERY` | the credential kind — `ws-ticket` |
| `AUTHN_CLAIM_PROVENANCE` | the ctx field — `ctx.userId` |

Guard rails, all compile errors:

- `check` must be a **promoted** id — unknown ids and keep-warn ids (there is
  nothing to override) are rejected at config validation.
- `reason` is required and non-empty: overrides are self-documenting.
- An `allow` entry that matches **no current finding** is a *dead override*
  and fails the compile — fix or remove the stale entry. Override lists can
  never silently outlive the problems they suppressed.

## Reading the pushdown-cost report

`AUTHZ_PUSHDOWN_COST` is an `info` line per permission that pushes down to
SQL — it never fails a build, but it surfaces the query cost of an
authorization rule before you ship it:

```
[quickback:authz] AUTHZ_PUSHDOWN_COST: permission "section:inTree" lowers to 1 SQL subquery, recursive CTE depth 6 (sites: resource).
```

A high subquery count or a deep recursive CTE (from a recursive
[arrow](/define/arrows)) is a signal to reconsider the rule —
not an error, but worth a look.

## See also

- [Permissions](/define/permissions) — the `anyOf` / `allOf` /
  `not` DSL the empty / shadowed / not-over-resource checks analyze.
- [Arrows](/define/arrows) — recursive-CTE depth feeds the
  pushdown-cost report; the unbounded-recursion guard is a hard error.
- [Scopes & capability grants](/define/scopes) — the grant
  profiles the capability-ceiling check enforces.
- [Firewall](/define/firewall) — the `{ permission }` arms the
  ceiling check follows into the permission graph.
