---
title: Signals overview
description: How four independent signals aggregate into one izri/quality verdict — every weight, every hard rule, and what each severity does.
order: 10
---

# Signals overview

Izri computes four independent signals and aggregates them into a single `izri/quality` verdict. This page is the aggregation model in full: if you want to predict whether a given finding will block your merge, everything you need is here.

## The four children

| Check | Question |
| --- | --- |
| [`izri/scope`](/docs/signals/scope) | Does the diff match its stated intent? |
| [`izri/tests`](/docs/signals/tests) | Are the tests for this change any good — do they pass, run the changed lines, and check anything? |
| [`izri/hallucination`](/docs/signals/hallucination) | Is what the change says about itself true? |
| [`izri/visual`](/docs/signals/visual) | Did the rendered UI change? |

All four report through one GitHub check run, `Izri`, whose conclusion is the `izri/quality` umbrella verdict and whose body lists each signal.

## Two ways to fail

The umbrella aggregates the children through two independent mechanisms. Understanding which one fired is the whole diagnostic.

### 1. Hard rules — worst-of

One hard finding fails the umbrella regardless of every other signal. There is no averaging, no override, no threshold to clear. The list is deliberately short:

| Hard rule | Source | Why it's absolute |
| --- | --- | --- |
| `pr_type_forbidden_category` | scope | The diff changed a category the PR's declared type forbids. |
| `change_uncovered` | tests | Executed coverage shows no test ran the changed lines of a file. The silent-regression case. |
| `test_failure` | tests | The test run failed. One red test fails the verdict, however high the composite. |
| `runner_error` | tests | The run broke before producing results. No evidence never resolves to "safe". |
| `evidence_missing` | tests | The project uploads from CI and nothing arrived for this commit within the grace window. |
| *analyzer error* | any | An analyzer crashed. Signal-agnostic — triggered by the row's `error` status, not a finding category. |

An analyzer crash being a hard rule is deliberate. A crashed analyzer produces no findings, and "no findings" must never be mistaken for "nothing wrong."

A project can add opt-in hard rules through its [merge policy](/docs/configure/config-yml) — a minimum diff coverage (`block.diff_coverage_below`), zero-test runs, sensitive paths, a visual diff limit. They are off by default.

### 2. Soft signals — weighted composite

Everything else feeds a weighted average that must reach **70**.

| Signal | Weight |
| --- | --- |
| Hallucination score | 40 |
| Tests score — the lower of pass rate and diff coverage | 36 |
| Scope alignment | 24 |
| Visual diff | 15 |

Weights are relative, not fractions: with all four signals present, hallucination is 40 of 115. These are the defaults; a project can change them in its [merge policy](/docs/configure/config-yml).

Izri judges the delta a PR introduces, so every number here is about the changed lines. The rest of the project is context, never what's graded — coverage means diff coverage.

**Absent signals redistribute, they don't penalize.** Weights are proportionally renormalized across the signals that actually ran. A backend PR that skips `izri/visual` scores exactly as it would have if the visual signal didn't exist. Skipped is not zero.

## Severities

Every finding carries `info`, `warn`, or `error`. Only `error` blocks by default.

| Severity | Effect |
| --- | --- |
| `error` | Blocks. Hard rules are emitted at this level. |
| `warn` | Lowers its signal's score, except advisory test-quality findings, which are shown but not scored. Does not block on its own. |
| `info` | A "look here" pointer. Never blocks; costs 2 points where findings are scored. |

This is why `sensitive_path` doesn't fail your build — it is emitted at `info`. It flags that a diff touched something worth a human glance, which is not the same as saying the diff is wrong.

**An LLM can't block.** A finding backed only by a model's judgment is capped at `warn`, whatever severity the model assigned. Only findings with tool evidence — coverage, an import resolver, test results — can reach `error`.

## Full findings reference

Every category the analyzers emit, and what each one does:

### Scope

| Category | Severity | Meaning |
| --- | --- | --- |
| `pr_type_forbidden_category` | `error` | Change in a category the declared PR type forbids. **Hard rule.** |
| `pr_type_mismatch` | `warn` | The diff doesn't look like what the PR type claims. |
| `test_removal` | `warn` | Tests were deleted. |
| `scale_anomaly` | `warn` / `info` | The diff is much larger or smaller than the stated intent implies. |
| `structural_drift` | `info` | Module boundaries shifted. |
| `sensitive_path` | `info` | A path your config marks sensitive was touched. |

### Tests

| Category | Severity | Meaning |
| --- | --- | --- |
| `test_failure` | `error` | The test run failed. **Hard rule.** |
| `runner_error` | `error` | The run broke before producing results. **Hard rule.** |
| `evidence_missing` | `error` | No upload arrived from CI within the grace window. **Hard rule.** |
| `test_evidence_untrusted` | `warn` | Green evidence from a PR that edited what produced it. **Hard, opt-in** via `block.untrusted_evidence`. |
| `change_uncovered` | `error` | No test exercised the changed lines. **Hard rule.** |
| `change_partially_covered` | `warn` | Some changed lines were exercised, some weren't. |
| `low_diff_coverage` | `warn` | Under 60% of the added lines were executed. Named `low_overall_coverage` before September 2026. |
| `file_not_instrumented` | `info` | The coverage tool never saw the file. Could be a filter, could be a gap. |
| `diff_coverage_below` | `error` | Diff coverage is under the project's configured minimum. **Hard rule, opt-in** via `block.diff_coverage_below`. Silent when `change_uncovered` already fired. |
| `zero_tests` | `error` | A passing run executed no tests. **Hard rule, opt-in** via `block.zero_tests`. |
| `change_lacks_test` | `warn` | Advisory, not scored. A changed source file has no changed test importing it. |
| `weak_assertion` | `warn` | Advisory, not scored. An LLM judged the assertions too weak to catch a regression. |
| `assertion_free_test` | `warn` | Advisory, not scored. A test block this PR touched contains no assertion. |
| `tautological_assertion` | `warn` | Advisory, not scored. An assertion that cannot fail. |
| `test_mocks_subject` | `warn` | Advisory, not scored. A test mocks a module this PR changed that no test in the PR runs unmocked. |
| `test_passes_without_change` | `warn` | Advisory, not scored. A test this PR wrote passes with the change reverted. |

### Hallucination

| Category | Severity | Meaning |
| --- | --- | --- |
| `import_unresolved` | `warn` | An import on an added line matches no file in the repository. |
| `export_missing` | `warn` | A named import a first-party module does not export. |
| `dependency_undeclared` | `warn` | A bare import of a package no `package.json` declares. |
| `package_not_found` | `warn` | A newly added dependency the npm registry has never seen. |
| `package_suspicious` | `warn` | A newly added dependency under 30 days old with under 1,000 weekly downloads. |
| `env_undeclared` | `info` | An env var read that no template, env config or workflow declares. |
| `reference_unparsed_import` | `info` | An import whose specifier is computed (not a string literal) was not checked. |
| `reference_file_unparseable` | `info` | The file has syntax errors; its import list may be incomplete. |
| `change_unverified` | `warn` | Every selected test still passed with the change reverted: nothing depends on this change. |
| `verification_evidence_untrusted` | `warn` | The PR could have shaped its own test evidence; the revert check is inconclusive. |
| `verification_skipped_budget` | `info` | Too many tests selected for the revert check; nothing ran. |
| `analyzer_stage_error` | `info` | One analysis stage crashed; the others still reported. Appears on the signal that owns the stage. |

## The three umbrella states

| State | Meaning |
| --- | --- |
| `passing` | No hard rule fired, composite ≥ 70. |
| `failing` | A hard rule fired, or composite < 70. |
| `unknown` | Not enough signal — delta in flight, or every child skipped. |

Treat `unknown` as "wait", never as "go".

## Related

- [Your first verdict](/docs/start/your-first-verdict) — reading this on a real PR.
- [Verdicts and exit codes](/docs/reference/verdicts-and-exit-codes) — the CI mapping.
- [`.izri/config.yml`](/docs/configure/config-yml) — tuning the scope signal.
