Naming conventions

Write your net and ref-des house style down once as patterns, and check enforces it on every design.

Your team probably has a house style for net and reference-designator names: power nets look like +3V3, signal nets are UPPER_SNAKE, no ref des is reused. A conventions file writes that style down once, as patterns, and check enforces it on every design. This is the simplest way to add your own rules, and it needs no code.

Write a conventions file

A conventions file is a small YAML document naming one or more rules. Each rule is a set of allowed patterns and a set of exemptions:

# example.yaml
name: example
rules:
  - name: signal-net-naming
    severity: info
    why: "signal nets are UPPER_SNAKE, rails and stubs are exempt"
    allow: ["^[A-Z][A-Z0-9_]*$"]
    exempt: ["^\\+", "^GND"]
  • allow passes a name that matches any of these patterns.
  • exempt skips any name matching one of these patterns (here, rails starting with +, and GND).
  • name (top level) is the namespace, so the rule above appears in the catalog as example/signal-net-naming.

Run it

Compose the file into a run with --conventions. On a board whose names all conform, the rule runs and reports nothing:

$ agni check i2c-sensor --conventions example.yaml --rule example/signal-net-naming
no findings (1 rule(s) run)
6 subject(s) considered by 1 rule(s) (--verdicts for the detail)

Read the second line as well as the first. It says the rule looked at six names and had nothing to report. A rule that ran over nothing would say something different.

Point the same convention at a board that does not follow it and the non-conforming names surface:

$ agni check showcase.fires.kicad_sch --conventions example.yaml --rule example/signal-net-naming
findings by rule:
  example/signal-net-naming 2

first 2:
  [info] example/signal-net-naming: /USB_D+ (net name matches no allowed naming pattern)
  [info] example/signal-net-naming: /USB_D- (net name matches no allowed naming pattern)

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

Both flagged names carry a trailing character the allow pattern does not cover. Neither is exempt either. ^\+ exempts a name that starts with +, and neither of these starts with +. Tightening allow, or narrowing exempt, surfaces more names the same way.

In a project

A project writes the same content as the conventions: section of its project.yaml, and every run on one of its designs composes it with no flag. A team sharing one vocabulary across projects puts it in a project of its own, and the other projects name that project with extends. See Projects and designs. A --conventions file still works on a project’s design. For that run it replaces the project’s section, and the next section describes how.

Tool-generated names are exempt by default

CAD tools invent stub names for connections you never named: KiCad unconnected-(...), Net-(...), gEDA/EDIF N$… autonames. These are never your house style and are exempt automatically, so a convention only ever flags a name a human actually chose. You do not have to list them in exempt.

Scope of a match

By default a pattern matches the leaf of a qualified name. A hierarchical net like /amp2/CTRL is matched on CTRL, so one convention works across every sheet without encoding the hierarchy into the pattern.

A server default, and how a request replaces it

agni serve --conventions house.yaml makes that config the deployment’s default. Its rules join the catalog, and every surface that runs rules reads that catalog. Its lexicon becomes the default naming vocabulary. So everyone asking that server questions gets the house answer without doing anything.

A request may carry its own convention instead, and when it does it replaces the server’s for that request. Both halves go together, so the request’s rules replace the server’s rules and its lexicon replaces the server’s vocabulary. Nothing of the deployment’s convention survives into a request that named its own.

That has two consequences.

Reusing the server’s name is fine, and is the natural way to refine it. A config named house sent to a server whose default is also named house simply replaces it. (Before this was settled, that combination failed outright with a duplicate-source error.)

Replacing is not the same as adding. If your project wants the house rules plus its own, the config it sends has to contain both. That is deliberate. Picture a request asking “what does this board look like under MY vocabulary”. It should get exactly that answer. If the caller could not turn the deployment’s rules off, it could not ask the question. It also means a finding can disappear after you switch conventions because its rule stopped running, not because the design improved.

Only the convention is replaced. The built-in rules, and anything from --profile-path, are unaffected. A request’s --intent-path replaces the design’s intent the same way.

In the browser

The viewer has a vocabulary control in the top bar. It lists the convention configs sitting beside the open design, and picking one applies it to everything that runs rules from then on: the checks panel, the report, and a review run. For a design whose project declares conventions it offers no file, because the project’s conventions are already in every run.

The bar always says which vocabulary the answers on screen were computed under, and looks different while a request convention is in effect, so you can tell a finding the design fixed from one whose rule stopped running (the same caution as above).

Switching discards the findings already on screen rather than keeping them, since they were computed by asking a different question.

The Lexicon panel sits in a tab beside Rules. It lists each vocabulary the run uses to read names, and where each entry came from. It edits a draft of the convention as written, so the draft keeps the convention’s rules. Add an entry, take one out, turn a vocabulary off or switch the rail inference, and the list updates. When you take a single built-in out, the panel writes that vocabulary with replace: true and states its other entries. A convention has no other way to drop a built-in. A built-in ref-des prefix cannot be taken out, since replace leaves the prefix table alone.

Try shows the nets whose roles would change and the findings that would appear or disappear. agni lexicon --try prints the same rows, and a row selects its net or part. A draft changes nothing on the page until you press Apply. Apply puts the draft in the vocabulary bar for the rest of the session, and a comparison is read under it as well. Save YAML writes the file, and only the file in your project reaches other people and other commands. Until then --conventions names it on check, review, query, stats, diff and render. It works in the browser engine too, so the public demo has it. Net Properties shows why each role is there.

Teach it your vocabulary

The rules block above checks names against your policy. The lexicon block is the other half and tells the engine what a name means, so the built-in rules reason about your board in your words.

A net can carry several roles at once, and six of them are configurable. Three more net vocabularies name the I2C bus lines, a net left open on purpose, and the two halves of a differential pair. The pull-up rules judge the I2C lines. The pin vocabularies name supply pins, transistor terminals, a package’s exposed pad, a diode’s anode and cathode, and a regulator’s feedback and switch pins:

lexicon:
  net:
    rail:      { patterns: ["^HV_"] }        # added to the built-ins
    ground:    { patterns: ["^CHASSIS_"] }
    feedback:  { patterns: ["_FBK$"] }
    switching: { patterns: ["_HSD$"] }
    control:   { patterns: ["_SHDN$"] }
    gate_drive: { patterns: ["_VBOOST$"] }
    i2c:       { exclude: ["^DDC_"] }        # SDA/SCL by default, I2S lines excluded
    no_connect: { patterns: ["^OPEN_"] }     # NC_ and no_connect by default
    diff_pair:
      pairs:
        - { positive: "_PLUS", negative: "_MINUS" }  # _P/_N, _DP/_DN and +/- by default
    voltage:   { patterns: ["^(\\d+)P(\\d+)$"] }  # reads VDD_3P3 as 3.3V
    infer_rail_from_supply_pins: false       # on unless a convention turns it off
  pin:
    supply: { patterns: ["^PWR_"] }         # VCC/VDD/VIN... by default; VIN1A_D10, VBUS_DET excluded
    gate:   { patterns: ["^DRV$"] }
    source: { patterns: ["^SRC_"] }
    drain:  { patterns: ["^DRN_"] }
    exposed_pad: { patterns: ["^GND_PAD$"] }
    anode:     { patterns: ["^POS$"] }
    cathode:   { patterns: ["^NEG$"] }
    feedback:  { patterns: ["^FDBK[0-9]*$"] }
    switching: { patterns: ["^HSD[0-9]*$"] }
    through_pair:
      pairs:
        - { a: "IO{n}_L", b: "IO{n}_H" }  # A{n}/B{n}, {n}A/{n}Y, SDA1/SDA2 and SCL1/SCL2 by default

Patterns are RE2, matched case-insensitively against the leaf of a hierarchical name, so /DCDC/12V_SW is tested as 12V_SW. They are ADDED to the built-in set unless you say replace: true, which makes yours the whole vocabulary. replace: true with no patterns is how you turn a built-in role off entirely. exclude takes a name back out even when a pattern matches it, since RE2 cannot say “SDA, but not I2S0_RX_SDA” in one pattern. Exclusions are added and replaced the same way patterns are.

voltage reads the voltage stated in a net’s name. rail-not-classified, the absolute-maximum rules, cap-voltage and net.nominal_voltage all use that voltage. It works a token at a time, with the name split on anything but letters, digits, . and +, so VDD_3P3_EN is read as VDD, 3P3 and EN. Each pattern needs a capture group. The first group holds the whole volts and the first later group that matched holds the fraction, so the built-in ^\+?(\d+)V(\d+)?$ reads 3V3 as 3.3 and 12V as 12. A name whose tokens state two different voltages, such as 12V_TO_5V, reads as none.

Every role a net carries records why. A role from a pattern names the pattern, and an inferred rail names the supply pin it feeds. So you can trace an unexpected rail back to the line that made it one. The reason travels with the net’s roles in the design’s IR, so agni stats <design> --format json --mask design.nets prints it.

Rails inferred from the pins they feed

A rail named after its consumer, such as LPC_VCC or FAN_VDD, does not start with a supply word, so the rail patterns miss it. With infer_rail_from_supply_pins on, which is the default, a net is also a rail when a later token of its name matches the supply pin vocabulary AND the net feeds a power_in pin of a fitted part that is not a connector. The role’s reason names that pin. Both halves are needed. EN_VCC feeds no supply pin and is still a signal, a USB receptacle’s own VBUS pin marks where a cable delivers power rather than a load, and a supply pin left unwired on its own stub is no rail at all. Voltage-named rails such as CSIA_3V3 are not inferred. rail-not-classified still reports them, and you still declare them as the section below describes.

The four roles that mean “named after a rail, but not a rail”

A regulator’s pins are conventionally named for the supply they produce, so a 12V converter’s nodes are 12V_FB, 12V_SW, 12V_MODE1, 12V_VDRV. Every one of those matches the rail vocabulary on its prefix and none of them carries 12V. The engine tells them apart with four roles, feedback, switching, control and gate_drive. A net carrying any of them is not a rail:

role typical suffixes what it is safe to probe?
feedback _FB, _VSENSE the divider tap setting the output no, probing shifts regulation
switching _SW, _BOOT, _PHASE, _LX the power-stage node no, highest dV/dt net on the board
control _EN, _MODE1, _SEL enable and configuration inputs yes
gate_drive _VDRV, _VGATE the gate driver’s own supply yes

The first two are excluded from probe-point rules because probing them is harmful. The last two are excluded because they are not rails, so a rule quantifying over rails would be naming the wrong subject. If your house convention spells a switched output 3V3_SW rather than a switch node, narrow the switching vocabulary with replace and the rest keeps working.

The pin.feedback and pin.switching vocabularies read a regulator’s own pins, so a connector on a buck’s output reads as an outlet when no net is named for the converter. A house that calls the pin FDBK adds ^FDBK[0-9]*$ to pin.feedback. The net vocabularies of the same names read nets only, because a pin name carries no rail prefix for a pattern like _FB$ to end on.

pin.anode and pin.cathode apply to the diode family alone, and they match the whole pin name rather than its leaf, so a pin named A/K is not read. A diff_pair entry names a suffix pair rather than a pattern, because a rule has to work out the complement of a name. Its suffixes match at the end of the whole net name without regard to case, the longest positive suffix wins, and the complement keeps the name’s casing, so usb_p expects usb_n. A board that declares its pairs needs no suffix. An Allegro board’s declared pairs (net.group) pass diff-pair-naming on the declaration, before any name is read (agni issue 1360).

A through_pair entry names two pins that carry one signal across a part, such as a level translator’s A2 and B2. agni walk crosses such a part from one pin of a pair to its partner alone, rather than through every pin (agni issue 1338). {n} stands for a channel number and appears in both names or neither, and a name matches whole, without regard to case. A pair holds only on a part that has exactly one pin of the partner’s name, and never on a connector.

The engine ships its defaults for these same six vocabularies in this same schema. There is no separate list of built-ins to consult.

Component classes

A part’s class comes from two signals: its ref-des prefix (R, TP, RT) and the words in its part name and attributes (ferrite, tvs, crystal). The class block extends both, keyed by the class it teaches.

A third signal is the part-number family. Some part numbers belong to a common ESD or TVS family (TPD4E05U06, PESD5Z5.0F, ESDA25P35, USBLC6, VBUS54CV, SMAJ, and their neighbours). A part with one of those numbers is a tvs whether it sits under a diode, an IC or an unknown symbol. An ESD array is often drawn as a U part, and a TVS diode often carries nothing but its part number. The bare words tvs and esd only refine a diode, since an IC whose description mentions ESD protection is still an IC. families adds to the built-in list, and replace: true replaces it along with the patterns.

lexicon:
  class:
    thermistor: { prefixes: ["TH"] }         # your house writes TH12, not RT12
    zener:      { prefixes: ["Z"] }
    tvs:        { families: ["^acmeesd"] }   # a house part-number family the built-in list lacks

A board that plugs into another one has connectors facing nothing a user touches, and those are not a class. Whether a connector is the module socket or the cable entry depends on the product rather than the part, so it is declared per design in the components section of its design intent, and the exposure rules read it there. That declaration replaced the internal_connector class. A lexicon naming that class fails to load and says where the declaration goes.

A prefix is the leading run of letters of a ref-des, so TH matches TH12 and a part whose library symbol declares TH?. Prefixes are matched case-insensitively and are ADDED to the built-in table, and a prefix you list wins over a built-in one, so a house that writes F for its ferrites re-points F away from fuse. replace: true applies to patterns only and leaves prefixes alone, so use exclude_prefixes to take a built-in prefix out. A house whose D parts are displays rather than diodes writes diode: { exclude_prefixes: ["D"] }, and a D7 is then classed by its part name alone.

The family tag follows the class, so a TH12 answers both component.class(TH12, "thermistor") and component.class(TH12, "resistor"), exactly as an RT12 does.

Four things fail at load rather than reading as a convention that matched nothing:

  • a class the engine does not ship (unknown component class "esd_array"),
  • a prefix holding anything but letters (T1 can never match, because T1 is a ref-des),
  • one prefix listed under two classes,
  • an excluded prefix that is not one of that class’s built-in prefixes.

Values written as a bare number

A value with no unit and no multiplier, such as a capacitor marked 100, has no unit any standard agrees on. Only a resistor’s bare number is read, as ohms. value_unit says what a bare number means on another class, and replaces the built-in for the class it names:

lexicon:
  value_unit:
    capacitor: pF        # a bare 100 on a capacitor is 100 pF
    inductor: uH

The unit may carry a multiplier prefix, which scales only a bare number, so a capacitor marked 10u is still 10 µF. A unit the engine cannot read, or a class it does not ship, fails the load.

Part numbers, values and do-not-populate

A part’s number, its value and whether it is fitted are read from its attributes, and attribute names the keys:

lexicon:
  attribute:
    mpn:        { keys: ["Manufacturer_No"] }   # MPN, Manufacturer_PN, Part Number and others by default
    value:      { keys: ["Wert"] }              # Value and three aliases by default
    dnp:        { keys: ["DNF"] }               # dnp by default
    dnp_values: { keys: ["fit-no"] }            # yes, true, 1, y and DNP by default

Keys match without regard to case and are tried in order. The first key holding a non-empty value on the part gives the answer. Each list is added to the built-ins, after them, unless you say replace: true. A part is not fitted when any of its dnp keys holds one of the dnp_values, so KiCad’s own flag set to no does not hide a house DNP field that says DNP.

A KiCad symbol’s own fields (Part Number, DNP, a Description) are read off the symbol instance, while the component keeps only Value, MPN, Manufacturer, Footprint and Datasheet. That is deliberate. A part’s class is read from every component attribute, and a description such as “card edge connector” would otherwise turn an M.2 socket into a test connector. An EDIF part number stated on the library cell rather than the placed instance is read with the built-in keys only.

The class must be one the engine already has. A project cannot yet declare a class of its own, such as a house esd_array refining diode. Declaring one would open the class vocabulary, and the engine keeps that vocabulary closed. DECISIONS.md records what would change that. component.class answers with these class names, and the relation catalog lists them.

See how your names were read

agni lexicon <design> lists the vocabularies a run uses to read names. Each pattern appears under its vocabulary with its source. A source is built in, or the one convention that won, from the server, the project, the design or the request. When a replace: true switches a built-in off, the built-in is still listed and marked, so you can see it went.

--net says why one net has its roles, and --try reads the design again with a different conventions file and says what moves. The design below runs its sensor from its own regulator. The regulator’s output is SENSOR_3V3, and the design’s design.yaml adds a rail pattern to match that name. Tried without it, the sensor supply stops being a rail, and the I2C pull-ups that go to it stop counting:

$ agni lexicon examples/common/designs/lexicon-demo --net SENSOR_3V3
SENSOR_3V3
  rail (convention): the name matches the rail pattern _[0-9]+V[0-9]$

its name matches
  net.rail  pattern  _[0-9]+V[0-9]$  design (sensor-house)
$ agni lexicon examples/common/designs/lexicon-demo --try examples/common/designs/lexicon-demo/builtins-only.yaml
1 net(s) change roles
  net         roles_before       roles_after
  SENSOR_3V3  rail (convention)  -

2 finding(s) change
  change    rule         subject_kind  subject  message
  appeared  i2c-pull-up  net           SCL      I2C net has no pull-up resistor to a rail
  appeared  i2c-pull-up  net           SDA      I2C net has no pull-up resistor to a rail

Once the changes are the ones you meant, carry the file in the project’s conventions: section. A project’s or design’s convention reaches every command that reads the design. A --conventions file reaches the commands that take that flag, which are check, review, query and lexicon.

agni lexicon <design> --format yaml writes the whole effective vocabulary as a conventions file, every list stated in full, which is a starting point when you would rather edit the complete list than add to it.

The verify-lexicon agent skill walks these commands with you on your own board, comparing what agni read against your BOM and the nets you know, and ends with a summary that names none of the board’s parts or nets. Asking through an LLM says how to install it in Claude Code, Cursor or Copilot.

Where to go next

  • Checks and reports shows that conventions findings read like any other, and that --fail-on can gate on them.
  • CLI reference documents the --conventions flag and agni lexicon.