Checks and reports

Narrow what runs, read the report in different shapes, and follow a finding back to its source.

Getting started ran the whole catalog and read the first few findings. This page is about working the report: narrowing to what you care about, reading it in different shapes, and following a finding back to what the tool actually saw.

Everything below is a view onto one thing, which is what each rule concluded about each subject it looked at:

flowchart TB
    R["one rule"] --> S["each subject it examined"]
    S --> V["one verdict each:<br/>pass, fail, or a named<br/>reason it could not decide"]
    V -->|"the fails only"| F["findings"]
    V -->|"all of them"| C["the considered set"]
    F --> A["the default report<br/>the --fail-on gate<br/>the viewer's counts"]
    C --> B["the --verdicts table<br/>the html report"]

Where these commands run

The commands below name their sample with no path in front of it, so each one runs from the directory that sample lives in. Two samples appear, both checked into a clone of the engine repo:

Sample Run from What it is
showcase.fires.*, showcase.passes.* demo/ one small board that trips several rules, and its clean twin
designs/gateway examples/tutorial-project the tutorial’s synthetic board, with a team’s checklist wrapped around it

So the agni check lines below assume you are standing in demo/:

git clone https://github.com/panyam/agni
cd agni/demo

Point the same commands at your own board instead and nothing else changes.

Narrow what runs

By default every rule runs. Two flags cut that down.

One rule by name (--rule, repeatable):

$ agni check showcase.fires.kicad_pro --rule i2c-pull-up
findings by rule:
  i2c-pull-up            1

first 1:
  [error] i2c-pull-up: SCL (I2C net has no pull-up resistor to a rail)

1 finding(s) total
2 subject(s) considered by 1 rule(s) (--verdicts for the detail)

A whole group by tag (--tag key=value, repeatable). Every rule carries catalog tags (category, tier, and more), so you can run one family at a time:

$ agni check showcase.fires.kicad_pro --tag category=power
findings by rule:
  bulk-cap               2
  decoupling-present     2
  esd-protection         2
  input-protection       1
  reverse-blocking-absent 1
  test-point-coverage    2

first 10:
  [warning] bulk-cap: +3V3 (power rail has no bulk capacitor)
  [warning] bulk-cap: VBUS (power rail has no bulk capacitor)
  [warning] decoupling-present: +3V3 (power rail has no decoupling capacitor)
  [warning] decoupling-present: VBUS (power rail has no decoupling capacitor)
  [info] esd-protection: USB_D+ (externally-exposed signal net has no ESD protection)
  [info] esd-protection: USB_D- (externally-exposed signal net has no ESD protection)
  [warning] input-protection: VBUS (connector feeds a power input with no fuse or TVS in the path)
  [warning] reverse-blocking-absent: VBUS (connector feeds a power input with no reverse-blocking element in the path)
  [info] test-point-coverage: GND (rail carries no test point; bring-up and factory test cannot probe it)
  [info] test-point-coverage: VBUS (rail carries no test point; bring-up and factory test cannot probe it)

10 finding(s) total
29 subject(s) considered by 9 rule(s), 2 not considered (--verdicts for the detail)

Nine rules ran instead of the whole catalog, and the last line is where you read that. --tag category=connectivity runs the connectivity group, and so on. Combine --rule and --tag to build exactly the set you want.

Read the report in the shape you need

--format takes six values, and the first five are the findings report in different shapes:

--format what you get
text (the default) a per-rule summary
markdown a severity-organized report, worst first, for pasting into a review
csv one row per finding
json one object per finding, for tooling (see provenance below)
report the same report as JSON, the wire shape the web viewer consumes
html a self-contained page, and the one form that reports the considered set rather than the violations alone (below)

The markdown one:

$ agni check showcase.fires.kicad_pro --format markdown
# agni check — showcase.fires.kicad_pro

| severity | findings |
|---|---|
| error | 1 |
| warning | 6 |
| info | 4 |

11 finding(s), 83 rule(s) run.

## error

### i2c-pull-up — An I2C net (SDA/SCL) reaches no rail through a pull-up resistor.
- `SCL` — I2C net has no pull-up resistor to a rail (showcase.fires.kicad_sch)

## warning

### bulk-cap — A named power rail carries no capacitor at all (no bulk reservoir).
- `+3V3` — power rail has no bulk capacitor (showcase.fires.kicad_sch)
- `VBUS` — power rail has no bulk capacitor (showcase.fires.kicad_sch)

### decoupling-present — A power rail feeds power-input pins but has no decoupling capacitor on it.
- `+3V3` — power rail has no decoupling capacitor (showcase.fires.kicad_sch)
- `VBUS` — power rail has no decoupling capacitor (showcase.fires.kicad_sch)

### input-protection — A connector feeds a power-input pin directly with no fuse or TVS in the path.
- `VBUS` — connector feeds a power input with no fuse or TVS in the path (showcase.fires.kicad_sch)

### reverse-blocking-absent — A connector feeds a power input with no directional element blocking reverse flow.
- `VBUS` — connector feeds a power input with no reverse-blocking element in the path (showcase.fires.kicad_sch)

## info

### esd-protection — An externally-exposed signal net (on a connector) has no TVS device.
- `USB_D+` — externally-exposed signal net has no ESD protection (showcase.fires.kicad_sch)
- `USB_D-` — externally-exposed signal net has no ESD protection (showcase.fires.kicad_sch)

### test-point-coverage — A power rail or ground net has no test point, on a board that uses them.
- `GND` — rail carries no test point; bring-up and factory test cannot probe it (showcase.fires.kicad_sch)
- `VBUS` — rail carries no test point; bring-up and factory test cannot probe it (showcase.fires.kicad_sch)

The header line (11 finding(s), 81 rule(s) run) is your coverage receipt. It says how many rules actually ran, so a clean report is distinguishable from a report that had little to check. See “silence is not a pass” in Concepts.

See what was checked, not only what failed

A findings report lists violations, so a clean subject says nothing at all and you cannot tell it apart from one no rule looked at. --verdicts answers the other question:

$ agni check showcase.fires.kicad_sch --verdicts
esd-protection  2 fail, 1 pass
    fail            USB_D+  no TVS or Zener is in the net's series reach and no part on it
                            declares a datasheet ESD rating
    fail            USB_D-  no TVS or Zener is in the net's series reach and no part on it
                            declares a datasheet ESD rating
    pass            SCL     TVS D7 clamps the net

i2c-pull-up  1 fail, 1 pass
    fail            SCL  no rail is reachable from SCL through a resistor within 3 hops
    pass            SDA  SDA reaches rail +3V3 through R1

reverse-blocking-absent  1 fail
    fail            SDA  the connector reaches a power input through passives alone, so nothing
                         in the path passes current one way

bulk-cap  1 not-considered
    not-considered  VBUS  the rail continues onto a sheet this read did not open, so its
                          reservoir may be drawn outside it

crystal-load-caps  2 pass
    pass            Y1.1  capacitor C3 sits on terminal net XTAL1
    pass            Y1.2  capacitor C4 sits on terminal net XTAL2

decoupling-present  2 not-considered
    not-considered  +3V3  the rail continues onto a sheet this read did not open, so its
                          decoupling may be drawn outside it
    not-considered  VBUS  the rail continues onto a sheet this read did not open, so its
                          decoupling may be drawn outside it

diff-pair-naming  1 pass
    pass            USB_D+  the design carries the complementary net "USB_D-"

duplicate-net-name  11 pass
    pass            +3V3    passes because claims is 1, not >= 2
    pass            GND     passes because claims is 1, not >= 2
    pass            LED_A   passes because claims is 1, not >= 2
    pass            LED_EN  passes because claims is 1, not >= 2
    pass            SCL     passes because claims is 1, not >= 2
    pass            SDA     passes because claims is 1, not >= 2
    pass            USB_D+  passes because claims is 1, not >= 2
    pass            USB_D-  passes because claims is 1, not >= 2
    pass            VBUS    passes because claims is 1, not >= 2
    pass            XTAL1   passes because claims is 1, not >= 2
    pass            XTAL2   passes because claims is 1, not >= 2

duplicate-ref-des  13 pass
    pass            C3   the reader found no second physical part claiming "C3"
    pass            C4   the reader found no second physical part claiming "C4"
    pass            D2   the reader found no second physical part claiming "D2"
    pass            D6   the reader found no second physical part claiming "D6"
    pass            D7   the reader found no second physical part claiming "D7"
    pass            J1   the reader found no second physical part claiming "J1"
    pass            J2   the reader found no second physical part claiming "J2"
    pass            R1   the reader found no second physical part claiming "R1"
    pass            R3   the reader found no second physical part claiming "R3"
    pass            TP1  the reader found no second physical part claiming "TP1"
    pass            U1   the reader found no second physical part claiming "U1"
    pass            U2   the reader found no second physical part claiming "U2"
    pass            Y1   the reader found no second physical part claiming "Y1"

esd-clamp-not-tvs  2 not-considered, 1 pass
    not-considered  USB_D+  the net carries no clamp at all, so there is no device class to
                            characterise; esd-protection reports it
    not-considered  USB_D-  the net carries no clamp at all, so there is no device class to
                            characterise; esd-protection reports it
    pass            SCL     the net's clamp is TVS D7, which is the fast ESD device class this
                            rule asks for

i2c-pull-up-split-rail  1 pass
    pass            SDA  SDA is pulled to +3V3 by R1

i2c-redundant-pull-up  1 pass
    pass            SDA  SDA is pulled to +3V3 by R1

input-protection  1 not-considered, 1 pass
    not-considered  VBUS  the net continues onto a sheet this read did not open, so its fuse or
                          clamp may be drawn outside it
    pass            SDA   D6 guards the path from the connector to all 1 power entries it
                          reaches

label-alias-conflict  11 pass
    pass            +3V3    passes because labels is empty
    pass            GND     passes because labels is empty
    pass            LED_A   passes because labels is empty
    pass            LED_EN  passes because labels is empty
    pass            SCL     passes because labels is empty
    pass            SDA     passes because labels is empty
    pass            USB_D+  passes because labels is empty
    pass            USB_D-  passes because labels is empty
    pass            VBUS    passes because labels is empty
    pass            XTAL1   passes because labels is empty
    pass            XTAL2   passes because labels is empty

led-polarity  1 pass
    pass            D2  passes because led_reversed does not hold

nc-pin-connected  8 pass
    pass            LED_A   passes because no net.connections where pin.electrical_type ==
                            "no_connect" (2 examined)
    pass            LED_EN  passes because no net.connections where pin.electrical_type ==
                            "no_connect" (2 examined)
    pass            SCL     passes because no net.connections where pin.electrical_type ==
                            "no_connect" (3 examined)
    pass            SDA     passes because no net.connections where pin.electrical_type ==
                            "no_connect" (4 examined)
    pass            USB_D+  passes because no net.connections where pin.electrical_type ==
                            "no_connect" (2 examined)
    pass            USB_D-  passes because no net.connections where pin.electrical_type ==
                            "no_connect" (2 examined)
    pass            XTAL1   passes because no net.connections where pin.electrical_type ==
                            "no_connect" (3 examined)
    pass            XTAL2   passes because no net.connections where pin.electrical_type ==
                            "no_connect" (3 examined)

output-output-conflict  11 pass
    pass            +3V3    passes because drivers is 1, not >= 2
    pass            GND     passes because drivers is 1, not >= 2
    pass            LED_A   passes because drivers is 0, not >= 2
    pass            LED_EN  passes because drivers is 1, not >= 2
    pass            SCL     passes because drivers is 0, not >= 2
    pass            SDA     passes because drivers is 0, not >= 2
    pass            USB_D+  passes because drivers is 0, not >= 2
    pass            USB_D-  passes because drivers is 0, not >= 2
    pass            VBUS    passes because drivers is 1, not >= 2
    pass            XTAL1   passes because drivers is 0, not >= 2
    pass            XTAL2   passes because drivers is 0, not >= 2

power-input-not-driven  3 not-considered
    not-considered  +3V3  the net continues onto a sheet this read did not open, so its feed may
                          exist outside it
    not-considered  GND   the net continues onto a sheet this read did not open, so its feed may
                          exist outside it
    not-considered  VBUS  the net continues onto a sheet this read did not open, so its feed may
                          exist outside it

power-tap-conflict  11 pass
    pass            +3V3    passes because rails is empty
    pass            GND     passes because rails is empty
    pass            LED_A   passes because rails is empty
    pass            LED_EN  passes because rails is empty
    pass            SCL     passes because rails is empty
    pass            SDA     passes because rails is empty
    pass            USB_D+  passes because rails is empty
    pass            USB_D-  passes because rails is empty
    pass            VBUS    passes because rails is empty
    pass            XTAL1   passes because rails is empty
    pass            XTAL2   passes because rails is empty

rail-not-classified  1 pass
    pass            +3V3  the net carries the rail role, so the rail rules and
                          net.nominal_voltage answer over it (3.3V, 1 supply pin(s))

single-pin-net  11 pass
    pass            +3V3    net reaches 8 connections, so it is not a stub
    pass            GND     net reaches 19 connections, so it is not a stub
    pass            LED_A   net reaches 2 connections, so it is not a stub
    pass            LED_EN  net reaches 2 connections, so it is not a stub
    pass            SCL     net reaches 3 connections, so it is not a stub
    pass            SDA     net reaches 4 connections, so it is not a stub
    pass            USB_D+  net reaches 2 connections, so it is not a stub
    pass            USB_D-  net reaches 2 connections, so it is not a stub
    pass            VBUS    net reaches 4 connections, so it is not a stub
    pass            XTAL1   net reaches 3 connections, so it is not a stub
    pass            XTAL2   net reaches 3 connections, so it is not a stub

symbol-unresolved  16 pass
    pass            power:+3V3       the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            power:GND        the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            power:PWR_FLAG   the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            power:VBUS       the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            power:VBUS_PROT  the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            test:C           the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:Crystal     the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:FUSE        the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:HDR3        the reference resolved from the schematic's own lib_symbols
                                     and declares 3 pin(s)
    pass            test:LDO_REG     the reference resolved from the schematic's own lib_symbols
                                     and declares 3 pin(s)
    pass            test:LED         the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:MCU         the reference resolved from the schematic's own lib_symbols
                                     and declares 9 pin(s)
    pass            test:R           the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:TVS_DIODE   the reference resolved from the schematic's own lib_symbols
                                     and declares 2 pin(s)
    pass            test:TestPoint   the reference resolved from the schematic's own lib_symbols
                                     and declares 1 pin(s)
    pass            test:USB_CONN    the reference resolved from the schematic's own lib_symbols
                                     and declares 4 pin(s)

unannotated-components  13 pass
    pass            C3   designator "C3" is assigned, carrying no placeholder mark
    pass            C4   designator "C4" is assigned, carrying no placeholder mark
    pass            D2   designator "D2" is assigned, carrying no placeholder mark
    pass            D6   designator "D6" is assigned, carrying no placeholder mark
    pass            D7   designator "D7" is assigned, carrying no placeholder mark
    pass            J1   designator "J1" is assigned, carrying no placeholder mark
    pass            J2   designator "J2" is assigned, carrying no placeholder mark
    pass            R1   designator "R1" is assigned, carrying no placeholder mark
    pass            R3   designator "R3" is assigned, carrying no placeholder mark
    pass            TP1  designator "TP1" is assigned, carrying no placeholder mark
    pass            U1   designator "U1" is assigned, carrying no placeholder mark
    pass            U2   designator "U2" is assigned, carrying no placeholder mark
    pass            Y1   designator "Y1" is assigned, carrying no placeholder mark

unconnected-component  13 pass
    pass            C3   component's pins land on 2 net(s)
    pass            C4   component's pins land on 2 net(s)
    pass            D2   component's pins land on 2 net(s)
    pass            D6   component's pins land on 2 net(s)
    pass            D7   component's pins land on 2 net(s)
    pass            J1   component's pins land on 4 net(s)
    pass            J2   component's pins land on 3 net(s)
    pass            R1   component's pins land on 2 net(s)
    pass            R3   component's pins land on 2 net(s)
    pass            TP1  component's pins land on 1 net(s)
    pass            U1   component's pins land on 3 net(s)
    pass            U2   component's pins land on 9 net(s)
    pass            Y1   component's pins land on 2 net(s)

unspecified-pin-with-driver  1 pass
    pass            LED_EN  passes because no net.connections where pin.electrical_type ==
                            "unspecified" and pin.declared and not (conn.virtual) (2 examined)

143 verdicts across 25 rule(s), 130 pass, 4 fail, 9 not-considered

The table groups by rule and tallies each one, so i2c-pull-up reports one fail and one pass rather than one finding and a silence. SDA is fine, and it says so and names the resistor and the rail holding it up, which is the line a findings report cannot print.

not-considered is the third answer, and the one worth reading closely. bulk-cap reaches VBUS and declines, because the rail continues onto a sheet this read did not open, so its reservoir may be drawn outside it. A findings report shows that net exactly as it shows a net that passed.

It honours --format text|csv|json|html, and --format html turns it on by itself, since the HTML report has no findings-only form. The CSV carries a verdict_id per row (i2c-pull-up:(net:SDA)), plus context (the entities to look at, as role=ref pairs) and terms (the values a conclusion rests on):

$ agni check showcase.fires.kicad_sch --verdicts --format csv
verdict_id,url,rule,outcome,subjects,statement,context,terms,reason
i2c-pull-up:(net:SCL),,i2c-pull-up,fail,net:SCL,no rail is reachable from SCL through a resistor within 3 hops,,hop limit=3,
i2c-pull-up:(net:SDA),,i2c-pull-up,pass,net:SDA,SDA reaches rail +3V3 through R1,pull-up=R1|rail=+3V3,,
i2c-pull-up-split-rail:(net:SDA),,i2c-pull-up-split-rail,pass,net:SDA,SDA is pulled to +3V3 by R1,pull-up=R1|rail=+3V3,pull-ups=1,

That is one rule out of the table, which runs to about 140 rows on this board.

Paste an id into a running viewer as ?verdict=<id> and it opens on that verdict with the proof drawn: the subject in focus, the resistor and rail behind it.

It is a separate table, not extra rows. --format csv without --verdicts is unchanged, so anything already reading the findings CSV keeps working and never sees a pass counted as a defect.

Only some rules report one so far. A rule missing from the output is declining to say what it looked at, which is not the same as reporting that it looked at nothing. Expect the table to be thin until more of the catalog converts.

Follow a finding to its source

Every finding carries provenance: the record of exactly what the tool saw. --format json exposes it:

$ agni check showcase.fires.kicad_pro --format json --rule i2c-pull-up
{
  "findings": [
    {
      "rule": "i2c-pull-up",
      "severity": "error",
      "subject": {
        "kind": "net",
        "ref": "SCL",
        "pin": "",
        "netId": "9de85e683cf7",
        "busId": ""
      },
      "message": "I2C net has no pull-up resistor to a rail",
      "inconclusive": false,
      "provenance": {
        "sourceFile": "showcase.fires.kicad_sch",
        "span": null,
        "nativeId": "",
        "nativeIdKind": ""
      },
      "sheets": [
        "/"
      ],
      "locateReason": "LOCATE_REASON_UNSPECIFIED",
      "datasheets": [],
      "context": []
    }
  ],
  "skipped": [],
  "verdicts": []
}
  • subject is what the finding is about: a net (SCL), a component (its ref des), or a component pin.
  • provenance.sourceFile is the file the fact came from. For datasheet-backed rules the message also names the datasheet page and table (see Datasheets).

This is why you never have to take a finding on faith. It points at the net, pin, or datasheet row you can go open yourself.

Start from the thing instead of the finding

In the viewer you can also go the other way. Click a part, a net or a bus, on the drawing or in a query result, and the selection bar tells you how many findings already name it, split by severity. Clicking that count opens them in the checks panel. A query’s results footer does the same for every entity the query returned at once, so “what is flagged on the twelve things I just searched for” is one number.

Nothing re-runs when you click. These are the findings from the run you already have, filtered, which is what keeps them consistent with the report beside them.

Two things sit beside the count rather than in it. “N unresolved” means a rule examined this thing and could not decide, which is never a pass and never a failure; the message says what it could not resolve, and often you can clear it by supplying that. “N rule(s) could not run” means rules you selected were gated before they evaluated, on this whole design, so they report nothing anywhere. Hover for which and why.

Read the wording, not just the number. A count is only meaningful once the rules have actually run, so the bar says “not checked yet” rather than “no findings” before you press Run, and reports a half-finished run as a floor (“2 so far”).

A count beside one entity is also narrower than a report, in two ways it names on screen: it covers only the rules you have selected, and only findings that name that entity. A rule about the whole design has no single subject and will never appear there. A rule comparing two pins is filed under one of them, so the other pin looks quiet in the count even though the finding names it.

The finding itself does say so. A message that names an entity other than its subject carries that entity as a chip you can click, so you can get from the sentence to the other end. An entity view tells you where to look. It is not a statement that anything is fine.

Understand a rule

Each rule ships with a short explainer describing what it looks for and why the condition matters electrically. In the viewer it appears beside the finding. On the command line the rule name is the handle you look it up by. The catalog is open, so your team can add house rules on top of the built-ins. Naming conventions is the simplest form of that, configured without any code, and interface profiles are the same idea for a bus: declare its signals, get a rule per requirement.

Here is one, in full, so you know what to expect before you go looking. Every rule in the rule catalog carries the same shape: what it checks, why it matters in the circuit, and what its silence does and does not mean. That last part is the one worth reading before you trust a green result.

cap-voltage, a capacitor's rating against the rail it sits on

Remedy

Fit a capacitor rated above the worst rail it touches, with the derating the design’s own rules call for. On ceramics, allow for the capacitance lost under applied DC bias as well.

What it means

A component classed as a capacitor, joined by MPN to a seeded datasheet spec carrying a rated voltage (VDC/WV/VR-family symbol or a “Rated Voltage” row), sits on a rail whose declared voltage times the derate factor (1.25) exceeds that rating.

a cap whose derated rail exceeds its rating is flagged; margin is fine

Why engineers want it

This is the stakeholder-named cap-voltage rule: the classic review checklist item (“is every cap rated for its rail, with margin?”) turned into a check whose limit is the vendor’s number with provenance, not a rule-of-thumb constant in code. The finding cites the datasheet page/table so the margin is verifiable, on the datasheet layer’s whole posture (docs/20).

Evidence honesty

Every untrusted input is a skip, never a guess: no MPN / unseeded MPN / no seeded corpus; no machine-comparable rated-voltage row (docs/20 comparison semantics); units other than “V”; a rail with neither a max_voltage attribute nor a name-derived nominal. The worst (highest) known rail among the cap’s nets governs.

Query structure

Spec-authored (docs/19 “a rule is a value”); the join and float compare live in the cap_voltage_detail SpecFunc, which returns the violation sentence or “”, so the rule body stays AST and the derived Reads carry the param join as named relations.

select C in components where class(C) == capacitor
  and cap_voltage_detail(C) != ""    // Vrated < worst_rail_V x 1.25, seeded and comparable

Reads: param.cap_rated_voltage, net.max_voltage, component.mpn, component.class, on_net. Tier R.

A rule that could not evaluate never reports a pass. It reports not-applicable, needs-data, or inconclusive, each naming what was missing. That distinction is the whole reason a clean report from this tool is worth something, and it is why the cards spend as much space on absence as on the check itself.

Gate a build

--fail-on <severity> exits non-zero when any finding sits at or above the threshold, so check gates CI:

$ agni check showcase.fires.kicad_pro --fail-on error
exit 2

and its passing twin:

$ agni check showcase.passes.kicad_pro --fail-on error
exit 0

That gate reads severity, which is a statement about consequence. It has nothing to say about the distinction the section above is built on: a check that decided, versus one that never ran. A design whose datasheet corpus moved keeps passing this gate, because a rule that could not evaluate produces no findings and no findings is what clean looks like.

review gates on the other axis:

agni review designs/gateway --checklist review.yaml --fail-on-outcome fail
agni review designs/gateway --checklist review.yaml --min-answered 13

--min-answered counts the items that produced an answer (pass, fail, provisional, computed-n/a), which is stricter than the covered count the report also prints. Covered subtracts only not-automated, so an item whose rule is present and whose inputs are gone still counts as covered. Both gates are off by default, and a tripped one exits 2 where a failed run exits 1. See the CLI reference for the full vocabulary.

Where to go next

The HTML report

agni check --format html > report.html writes one self-contained page: what each rule looked at, what it concluded, and what to do about the parts that failed.

agni check --format html \
  --server http://localhost:8080 \
  --mount board=. mount://board/design.kicad_sch > report.html

Rules with something to act on come first and open expanded; rules that cleared everything collapse to a one-line summary you can expand to check their working. That ordering is the whole design: a board with three problems and two thousand passes has to show you the three problems.

A rule that reports violations without stating what it examined is labelled as such, and its rows are captioned “absence here is not evidence of correctness”. Presenting a failure list beside a considered set as though they were the same kind of answer is the false-coverage claim this whole layer exists to remove, and a report is where it would be most convincing.

Links are emitted only when they are real. --server says where the viewer is. Against a URL, the other half is that the mount was DECLARED, with --mount or in agni.yaml, rather than minted for this run: a minted name means nothing on a server that was not started with it, so it gets no links rather than links resolving on nobody’s server. Either way the reason is printed.

--server self removes that second half rather than satisfying it. One process reads the design and serves it, so the table the links name is the table being served and a minted mount is as linkable as a declared one. It blocks until Ctrl-C, which is the honest lifetime of those links: they resolve for exactly as long as the server that mints them is up.

What the server is asked, and the three answers it can give

Declaring the mount says the operator named it, which is not the same as the server agreeing about it, so a --server <url> run also asks that server for its mount table. A name served from a different root means every link would open a different board, and those links are dropped. A server that does not answer leaves the question open rather than settling it, so the links stay and the run says they went unverified, which keeps the case where a report is written before the viewer is up.

A report with no links used to look like a broken renderer, because nothing named which half was missing.

Each link also carries the design’s content hash, so the viewer can say a link was computed against different bytes rather than silently highlighting whatever now sits at that subject.

A link names the ENTRY the design declares, not the argument you typed, and so does its hash. Both halves come from one resolution, so pointing check at the design folder, at a declared companion, or at the entry file produces the same link. That matters twice over. A folder is not a file the viewer can open, so a link built from the folder argument used to load nothing at all. And a companion has its own bytes, so a link built from the companion argument used to carry the entry’s hash and read as a revision mismatch on a design that was in sync.

Opening such a link, the viewer compares that hash against the revision it just read and says so before it draws anything. A match is silent. Different bytes get a banner saying the highlight may be about a different net, because a verdict id is built from a rule name and a subject ref and resolves against an edited design just as readily. A server that could not hash the file gets its own weaker banner rather than the benefit of the doubt, since a link nobody could check has not been checked, and treating it as though it had is the false confidence the hash exists to remove.

The easiest way to get all of this right is not to assemble it by hand. agni open <design> serves the board and prints the matching agni check --mount … --server … line, and because one process mints the mount and serves it, the two cannot disagree.

The page needs no JavaScript and loads nothing from the network, so it survives being emailed, committed, or opened from a file:// path.