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": []
}
subjectis what the finding is about: anet(SCL), acomponent(its ref des), or a componentpin.provenance.sourceFileis 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.
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 ¶
- Datasheets: turn on the rules that compare your design against a part’s real limits.
- Comparing revisions: diff two versions of a design.
- CLI reference: every flag in one place.
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.