8. Write your checklist

The questions your team asks of every board, bound to the engine so they answer themselves.

Most teams already have a review checklist. It is usually a spreadsheet, and going through it is usually somebody’s afternoon. Much of it is mechanical. It asks questions, and the design file already holds the definite answer to each one.

The Jetson project’s project.yaml holds that checklist as its review checklist. It is written so the mechanical items answer themselves and the rest are still visible as work for a human.

agni check answers “what is wrong with this board”. agni review answers “which of our questions did we actually answer”. That is a different question, and a more useful one when you are deciding whether a board is ready.

The shape

checklists:
  review:
    name: Jetson baseboard design review
    areas:
      - name: Power
        items:
          - id: "P1"
            title: every rail carries a bulk capacitor
            description: A rail with no bulk capacitance browns out on a load step.
            rule: bulk-cap

checklists maps a name to a checklist, so a project can carry several. The first one written is the project’s default.

Areas group items the way your existing checklist groups them. Each item keeps its own id, so an item that has been “P1” in your process for years is still P1 here and nobody has to relearn numbering.

Four ways to bind an item

rule: hands the item to a catalog rule. It can be any rule from any tier, whether built in, added by your conventions section, contributed by a profile, or declared as intent.

- {id: "P2", title: every rail carries decoupling, rule: decoupling-present}
- {id: "H1", title: active-low signals use the overbar, rule: jetson/active-low-overbar}
- {id: "A1", title: each rail sits at its declared voltage, rule: intent/voltage-domain-mismatch}

profile: hands it to an interface profile, so the item covers everything that profile requires, and reads as unevaluated rather than passed when the interface is absent.

- {id: "I1", title: every PCIe link is complete, profile: PCIe}
- {id: "I2", title: the CAN interface is complete and terminated, profile: CAN}

query: is for a question no shipped rule asks. You write the datalog inline.

- id: "H2"
  title: every test point sits on a net somebody named
  query:
    match: 'component.class(?r, "test_point"), pin.net(?r, ?p, ?n), str.prefix(?n, "Net-(") => ?r, ?n'
    subject: r
    message: '{r} probes {n}, a net nobody named'

When nobody labels a net, KiCad names it after one of its pins, as in Net-(J18-PadH01). A bring-up engineer holding a probe and a schematic cannot find that net by name, so this team asks that every test point sit on a net someone named.

note: is for a question nothing automated can answer. The item is still on the checklist and reports that no automated check answered it.

- {id: "H3", title: the assembly drawing lists a torque spec for every standoff,
   note: manual review against the mechanical drawing package}

The temptation with an unautomatable item is to drop it, and then it is not on the checklist at all. A note: keeps the question visible and says who owns it.

The query trap

Read the H2 query again. It matches test points on nets that nobody named.

A query binding reports whatever it matches as findings, and findings mean the item failed. So the query has to match the violation. Write the healthy case instead, phrasing it the natural way as “every test point sits on a named net”, and you get an item that fails on a good board and passes on a bad one.

A two by two grid. A checklist query that matches the violation passes on a healthy board and fails on a bad one, which is correct. A query phrased the natural way, matching the healthy case, does the reverse: it fails on a healthy board and passes on a bad one. A query binding reports what it matches, and a finding means the item failed, so the polarity has to be the violation. on a healthy board on a bad board the query matches the violation which is what a binding needs pass fail the query matches the healthy case phrased the natural way, and inverted fail pass a query binding reports what it MATCHES, and a finding means the item failed so an inverted item is quiet: on a healthy board it reads as a finding nobody got round to

This authoring mistake is easy to make, and it is quiet, because on a healthy board an inverted item just looks like a finding you have not got round to.

Running it

Naming the design is the whole command. This project already declares every input a review needs. project.yaml carries its checklists and its naming vocabulary, profiles/ its interfaces, params/ its part limits, and designs/jetson/design.yaml names the schematic to read, the board beside it and the board’s intent.

$ agni review designs/jetson
# Review: Jetson baseboard design review

Design: `designs/jetson`

**6 pass, 7 fail, 1 n/a, 1 not-automated (of 15)**

## Power

2 pass, 3 fail, 0 n/a, 0 not-automated (of 5)

| # | Title | Outcome | Detail |
|---|-------|---------|--------|
| P1 | every rail carries a bulk capacitor | fail | bulk-cap: Net-(D1-C) (power rail has no bulk capacitor) |
| P2 | every rail carries decoupling | pass |  |
| P3 | the input is protected against reverse polarity | fail | reverse-blocking-absent: VCC_IN (connector feeds a power input with no reverse-blocking element in the path) |
| P4 | no part is operated above its absolute-maximum supply voltage | pass |  |
| P5 | every rail is probeable during bring-up | fail | test-point-coverage: /CSI/CSIA_3V3 (rail carries no test point; bring-up and factory test cannot probe it); test-point-coverage: /CSI/CSIA_5V (rail carries no test point; bring-up and factory test cannot probe it); test-point-coverage: /CSI/CSIA_I2C_VCC (rail carries no test point; bring-up and factory test cannot probe it); (+33 more) |

## Interfaces

1 pass, 2 fail, 1 n/a, 0 not-automated (of 4)

| # | Title | Outcome | Detail |
|---|-------|---------|--------|
| I1 | every PCIe link is complete | pass |  |
| I2 | the CAN interface is complete and terminated | not-applicable | interface not present on this design |
| I3 | exposed signals have ESD protection | fail | esd-protection: /SFP/SFI_CONN_RX_N (externally-exposed signal net has no ESD protection); esd-protection: /SFP/SFI_CONN_RX_P (externally-exposed signal net has no ESD protection); esd-protection: /SFP/SFI_CONN_TX_N (externally-exposed signal net has no ESD protection); (+7 more) |
| I4 | every I2C bus has pull-ups | fail | i2c-pull-up: /CSI/I2C_MUX_SCL (I2C net has no pull-up resistor to a rail); i2c-pull-up: /CSI/I2C_MUX_SDA (I2C net has no pull-up resistor to a rail); i2c-pull-up: /CSI/I2C_{CAM}.SCL (I2C net has no pull-up resistor to a rail); (+25 more) |

## House style

0 pass, 2 fail, 0 n/a, 1 not-automated (of 3)

| # | Title | Outcome | Detail |
|---|-------|---------|--------|
| H1 | active-low signals use the overbar | fail | jetson/active-low-overbar: /USB Debug, PD/PDC_I2C1_IRQn (net name matches no allowed naming pattern); jetson/active-low-overbar: /USB Debug, PD/PDC_I2C2_IRQn (net name matches no allowed naming pattern) |
| H2 | every test point sits on a net somebody named | fail | review/H2: TP109 (TP109 probes Net-(J18-PadH01), a net nobody named); review/H2: TP110 (TP110 probes Net-(J18-PadK01), a net nobody named); review/H2: TP111 (TP111 probes Net-(J18-PadK40), a net nobody named); (+29 more) |
| H3 | the assembly drawing lists a torque spec for every standoff | not-automated | manual review against the mechanical drawing package |

## Board

1 pass, 0 fail, 0 n/a, 0 not-automated (of 1)

| # | Title | Outcome | Detail |
|---|-------|---------|--------|
| B1 | no track is below the board's minimum width | pass |  |

## Architecture

2 pass, 0 fail, 0 n/a, 0 not-automated (of 2)

| # | Title | Outcome | Detail |
|---|-------|---------|--------|
| A1 | each rail sits at its declared voltage | pass |  |
| A2 | every connector declared internal exists | pass |  |

Each item carries its outcome and the evidence behind it. An item bound to a rule that fired shows what fired and on which net, so the checklist row and the debugging detail are the same artifact. Every tier you added appears in it. P4 passes on the transcribed NTS0102, I1 on the project’s PCIe profile, H1 fails on the naming rule, and A1 and A2 pass on the declared intent. I2 reads not-applicable, because this board carries no CAN transceiver and an absent interface is not a passed one.

On stderr it says which checklist it picked:

$ agni review designs/jetson
note: running the checklist "review" projects/jetson declares; pass --checklist to run a different one.

A run’s outcomes do not say which checklist scored them, so a checklist you did not type has to announce itself. --checklist <name> runs another one the project declares, and a value ending in .yaml is read as a manifest file instead, for a checklist that lives outside the project.

A page you can walk

The markdown above is for reading in a terminal and pasting into a review. --format html writes the same checklist as a self-contained page instead:

agni review designs/jetson --format html --server self > review.html

The page differs from the markdown in two ways. Every finding is listed per item, where the Detail cell above caps at three, so a rule that fired on thirty-six rails shows thirty-six. And each finding links to its proof, so clicking the net opens the board with the checks run and that subject in focus.

--server self starts the viewer over this run’s own mount table and blocks until Ctrl-C, so the links cannot point somewhere the report was not read from. It needs the viewer’s assets the way agni open does in rung 3. You can also name a server someone else is running. Then the links need the same two things check --server <url> needs, a viewer address and a mount you DECLARED. When the command withholds a link, it prints the reason.

Items about the board, not the netlist

Some questions are about copper, and a schematic has none:

- {id: "B1", title: no track is below the board's minimum width, rule: track-width}

In the run above it resolves, because design.yaml declares the board as a companion of the schematic and the run reads both. To see what the item does with no copper, read the schematic on its own with --as-named, the flag that says “exactly the file I named, not the design it belongs to”:

$ agni review --as-named designs/jetson/jetson-agx-thor-baseboard.kicad_sch --checklist review
| B1 | no track is below the board's minimum width | not-applicable | design carries no board geometry (WS1-006 sidecar) |

Nothing about the item changed. Only its input changed. --board-path does the same job for a board that is not a declared companion: a fab’s returned file, or a layout under review that has not been added to the design yet.

The n/a is not hiding a failure and it is not standing in for a pass. The question had nothing to evaluate. Only supplying the copper would show which one it would have been. An item that scored pass on the schematic alone would have been wrong, and nobody would ever have checked it again.

Growing it

Start with the items your team already argues about, not with the ones that are easiest to automate. An item bound to nothing is still worth adding as a note:, because the checklist is then complete and the automation gap is measurable rather than invisible.

For a checklist of this size, hand-authoring the YAML is right. Teams running a few hundred items usually move to one file per item with the manifest generated from them, so items can be edited in parallel without conflicting.

Next

Read the verdicts explains why “7 fail” is a much better number than it looks.