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 covers narrowing the report to what you care about, reading it in different shapes, and following a finding back to what the tool actually saw.

Each rule looks at a set of subjects and reaches a conclusion about each one. Everything below is a view onto those conclusions:

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 run each one from the directory that holds its sample. 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.

Run one rule by name with --rule, which is 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)

Run a whole group by tag with --tag key=value, also 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 says so. --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), 88 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 reads 11 finding(s), 81 rule(s) run, and it is your coverage receipt. It says how many rules actually ran, so you can tell a clean report 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. You cannot tell it apart from a subject that no rule looked at. --verdicts answers the other question:

$ agni check showcase.fires.kicad_sch --verdicts
bulk-cap  2 fail
    fail            +3V3  the rail carries no capacitor at all
    fail            VBUS  the rail carries no capacitor at all

decoupling-present  2 fail
    fail            +3V3  no capacitor sits on the rail, which feeds at least one power-input
                          pin
    fail            VBUS  no capacitor sits on the rail, which feeds at least one power-input
                          pin

esd-protection  2 fail, 2 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
    pass            /SDA     TVS D6 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

input-protection  1 fail
    fail            VBUS  the connector reaches power input VBUS with no fuse crossed and no
                          clamp on the path, and U1's datasheet was not read, so whether it
                          limits its current is unverified

reverse-blocking-absent  1 fail
    fail            VBUS  the connector reaches a power input through passives alone, so nothing
                          in the path passes current one way, and U1's datasheet was not read,
                          so whether it blocks reverse current is unverified

test-point-coverage  2 fail, 1 pass
    fail            GND   rail carries no test point; bring-up and factory test cannot probe it
    fail            VBUS  rail carries no test point; bring-up and factory test cannot probe it
    pass            +3V3  passes because TP1.1 is component.class == "test_point"

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

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            /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            /XTAL1   passes because claims is 1, not >= 2
    pass            /XTAL2   passes because claims is 1, not >= 2
    pass            GND      passes because claims is 1, not >= 2
    pass            VBUS     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, 2 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
    pass            /SDA     the net's clamp is TVS D6, 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

label-alias-conflict  11 pass
    pass            +3V3     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            /XTAL1   passes because labels is empty
    pass            /XTAL2   passes because labels is empty
    pass            GND      passes because labels is empty
    pass            VBUS     passes because labels is empty

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

nc-pin-connected  11 pass
    pass            +3V3     passes because no net.connections where pin.electrical_type ==
                             "no_connect" (8 examined)
    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)
    pass            GND      passes because no net.connections where pin.electrical_type ==
                             "no_connect" (19 examined)
    pass            VBUS     passes because no net.connections where pin.electrical_type ==
                             "no_connect" (4 examined)

output-output-conflict  11 pass
    pass            +3V3     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            /XTAL1   passes because drivers is 0, not >= 2
    pass            /XTAL2   passes because drivers is 0, not >= 2
    pass            GND      passes because drivers is 1, not >= 2
    pass            VBUS     passes because drivers is 1, not >= 2

power-input-not-driven  3 pass
    pass            +3V3  1 driving pin(s) on the net feed its 5 power-input pin(s)
    pass            GND   a power flag on the net asserts it is driven, feeding 11 power-input
                          pin(s)
    pass            VBUS  a power flag on the net asserts it is driven, feeding 2 power-input
                          pin(s)

power-tap-conflict  11 pass
    pass            +3V3     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            /XTAL1   passes because rails is empty
    pass            /XTAL2   passes because rails is empty
    pass            GND      passes because rails is empty
    pass            VBUS     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            /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            /XTAL1   net reaches 3 connections, so it is not a stub
    pass            /XTAL2   net reaches 3 connections, so it is not a stub
    pass            GND      net reaches 19 connections, so it is not a stub
    pass            VBUS     net reaches 4 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  4 pass
    pass            +3V3     passes because no net.connections where pin.electrical_type ==
                             "unspecified" and pin.declared and not (conn.virtual) (8 examined)
    pass            /LED_EN  passes because no net.connections where pin.electrical_type ==
                             "unspecified" and pin.declared and not (conn.virtual) (2 examined)
    pass            GND      passes because no net.connections where pin.electrical_type ==
                             "unspecified" and pin.declared and not (conn.virtual) (19 examined)
    pass            VBUS     passes because no net.connections where pin.electrical_type ==
                             "unspecified" and pin.declared and not (conn.virtual) (4 examined)

154 verdicts across 26 rule(s), 141 pass, 11 fail, 2 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. The table says so, and names the resistor and the rail holding it up. A findings report cannot print that line.

not-considered is the third answer. esd-clamp-not-tvs reaches /USB_D+ and /USB_D- and declines. Those nets carry no clamp at all, so the rule has no device class to judge. It names esd-protection as the rule that reports them. A findings report shows those nets 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 behind a conclusion):

$ 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. The full table runs to about 150 rows on this board.

Narrowed to one rule, the same table is a bus sign-off sheet. agni check <design> --rule i2c-pull-up --verdicts --format csv (or --format html) gives one row per I2C net that the rule considered, passing and failing alike. A pull-up one resistor away reads SDA reaches rail VCC_3V3 through R1. One behind a series part names every net on the way, as SCL reaches rail VCC_3V3 along SCL -> [R2] -> BUF_NET -> [R3] -> VCC_3V3, in the same route spelling agni trace prints. A failing row states the hop limit of its search, since a pull-up four hops out is a different problem from no pull-up at all.

Paste an id into a running viewer as ?verdict=<id> and it opens on that verdict with the proof drawn, with the subject in focus and 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. When a rule is missing from the output, it is declining to say what it looked at. That differs from 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, a record of exactly what the tool saw, and --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": [],
      "verdictIds": [
        "i2c-pull-up:(net:/SCL)"
      ]
    }
  ],
  "skipped": [],
  "verdicts": [],
  "unexpandedHierarchy": [],
  "ioMap": []
}
  • subject is what the finding is about, either 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).

So you never have to take a finding on faith. Each finding points at a net, pin, or datasheet row, and you can go open it 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. The bar filters the findings of the run you already have, so they always agree 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. An unresolved answer is never a pass and never a failure. The message says what the rule could not resolve, and often you can clear it by supplying that. “N rule(s) could not run” means some selected rules were gated on this whole design before they evaluated, 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, and the bar says so on screen. The count 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 and says nothing about whether 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 you look it up by the rule name. 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, where you declare its signals and 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. Read that last part 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 is classed as a capacitor and joined by MPN to a seeded datasheet spec. The spec carries a rated voltage, as a VDC/WV/VR-family symbol or a “Rated Voltage” row. The component sits on a rail, and the rail’s 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 rule turns the classic review checklist item (“is every cap rated for its rail, with margin?”) 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. That follows the datasheet layer’s whole posture (architecture/datasheet-layer.md).

What it skips rather than guesses

Every untrusted input is a skip, never a guess: no MPN / unseeded MPN / no seeded corpus; no machine-comparable rated-voltage row (architecture/datasheet-layer.md, “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. Because a skip and a pass look the same to the spec, the rule reports violations only and states no considered set.

Query structure

Spec-authored (architecture/rules-and-checks.md, “A rule is a value”). The join and float compare live in the cap_voltage_detail SpecFunc. It returns the violation sentence or “”, so the rule body is still an 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. A clean report from this tool is worth something only because of that distinction. For the same reason, 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, and severity is a statement about consequence. The section above separates a check that decided from one that never ran, and severity says nothing about that distinction. Suppose a design’s datasheet corpus moves. The design keeps passing this gate, because a rule that could not evaluate produces no findings, and no findings looks like clean.

review gates on the other axis:

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

--min-answered counts the items that produced an answer (pass, fail, provisional, computed-n/a). That count is stricter than the covered count, which 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.

The HTML report

agni check --format html > report.html writes one self-contained page showing 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, and rules that cleared everything collapse to a one-line summary you can expand to check their working. The ordering exists because a board with three problems and two thousand passes has to show you the three problems.

A design that declares an IO map gets a section per device ahead of the rules, one row per declared pin with its function, the net the netlist puts there, the route to its far end and each IO-map rule’s outcome. Design intent shows the layout.

--query-sets adds the project’s query sets after the rules. The design’s project keeps its sets under queries/. Each set gets a Queries section, answered over the same read that the checks used. A review often wants the checks beside its own tables (a pin list, a ratio sheet). The flag puts both in one file attached to the review, rather than two exports merged by hand (agni issue 1190). The sections render as agni query --set <name> --format html renders them, and a project keeping no sets gets the checks alone with a note on stderr.

agni check --format html --query-sets designs/demo-board -o combined.html

Some rules report violations without stating what they examined, and the report labels them as such. Their rows are captioned “absence here is not evidence of correctness”. A failure list and a considered set are different kinds of answer. Presenting them side by side as one kind would claim coverage that the rule never checked, and a report would make that claim look most convincing.

Links are emitted only when they are real. --server says where the viewer is. Against a URL, the mount must also be 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 a run on a minted mount gets no links, rather than links that resolve on nobody’s server. Either way the reason is printed.

--server self makes a declared mount unnecessary. One process reads the design and serves it. The links name that process’s own mount table, so a minted mount is as linkable as a declared one. It blocks until Ctrl-C, because its links resolve for exactly as long as its server is up.

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

Declaring the mount says the operator named it. That does not mean the server agrees about it, so a --server <url> run also asks that server for its mount table. If the server serves the name from a different root, every link would open a different board, so those links are dropped. If the server does not answer, the question is still open. The run keeps the links and says they went unverified. That way a report written before the viewer is up still gets its links.

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 design’s declared ENTRY, 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. The viewer cannot open a folder, 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.

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

A link also names its rule and the verdict’s nets, parts and pins, as rule= and subject=. subject= uses the spelling --subject takes. The viewer then asks that one rule about those subjects alone rather than running the catalog over the whole board. Both are hints on top of the verdict id. A link without them still opens the same verdict by running the whole rule. Links in a report saved before the hints existed work that way. So does a link whose design has changed so much that the scoped answer no longer holds the verdict.

Don’t assemble all of this by hand. agni open <design> serves the board and prints the matching agni check --mount … --server … line. One process mints the mount and serves it, so the two cannot disagree.

A report on a hierarchical design says that its counts cover the top level alone. The EDIF reader extracts a hierarchical .edn’s top cell and skips the sub-cells it places, so every finding and every count on the page describes the top level. The check report and the checklist page both state this in their header, above the counts. They use the same sentence the CLI prints on stderr:

hier.edn is hierarchical and only its top cell was read. 1 sub-cell holding 1 instance was not extracted (SUB: 1). Counts and checks cover the top cell alone.

It names the five largest skipped cells and totals the rest. The list travels with the run (CheckDesignResponse.unexpanded_hierarchy, and the results document’s design record), so a report saved later from the viewer says it as well. A flat design gets no such line. With --flatten-hierarchy (Snapshot.flatten_hierarchy on the wire), the read covers the whole schematic and the line goes away. The line remains if a block places itself, because the walk stops there. The markdown and text output carry no header band, so on those formats only the stderr note says it.

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

Save a report from the viewer

Once every selected rule has run, the checks panel offers Save report and Save JSON. Save report downloads the same page that agni check --format html writes. Save JSON downloads the results document that agni check --results-out writes. agni results can re-render that document later, and the document names the engine build that produced it. Neither button runs anything again. The engine renders the run that the panel already holds. It goes through the same code the CLI calls, the RenderCheckReport rpc, so a saved file and the CLI’s differ only in when they were written and in how each names the design.

Save CSV downloads two files, <design>.findings.csv and <design>.verdicts.csv. Their rows match what agni check --format csv and agni check --verdicts --format csv write. The engine lays the run out as tables (the Tabulate rpc) and writes them (the Encode rpc), with the same code the CLI uses. So both guard any cell that a spreadsheet would read as a formula. The rows are in the panel’s rule order, which can differ from the CLI’s, and are otherwise the same. The Tables panel shows the same run as tables before you save, one tab per table, and saves whichever is on screen.

The Review panel’s Save report does the same for a checklist run. It downloads <design>.agni-review.html, the page agni review --format html writes. The page lists the checklist’s items in the manifest’s order, with every finding behind each. The engine renders the run that the panel shows, through the RenderReviewReport rpc. The CLI’s --format html calls the same rpc, so nothing is scored again and the two pages differ only in when they were written and in how each names the design.

Each finding on the checklist page links to its source verdict, by the id the run stamped on it. So the link opens the same proof as the check report’s row for that verdict. Some rules state no considered set, such as a query that declares no domain. A finding from one of them has no verdict to open, so the page lists it without a link.

On the browser-only demo the engine runs in the page, so saving a design you dropped sends nothing anywhere. Its rows link back into the viewer you saved it from. A design opened from your own files gets no links, and the page says why, because the browser keeps those files only in the tab that opened them.

Where to go next