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"]
allowpasses a name that matches any of these patterns.exemptskips any name matching one of these patterns (here, rails starting with+, andGND).name(top level) is the namespace, so the rule above appears in the catalog asexample/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 (
T1can never match, becauseT1is 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-oncan gate on them. - CLI reference documents the
--conventionsflag andagni lexicon.