The circuit as a language, or as TOML with names¶
A source
This page is a source of edakit – a concept, a prompt or the rules of a session –, shown as it is in the repository. Read it as such; the guides say what edakit does.
Closed on 2026-10-31: the sketch has done its work – decision 8 of design-as-text is taken – and two concepts carry it on: circuit-language-plan.md builds its decisions (TOML with names, pins by name or role, the resolved graph.json), circuit-dsl.md takes up the deferred language.
Status 2026-10-31: sketch of step 4.3 of the implementation plan, for decision 8 of
design-as-text.md, written on 2026-10-29 against edakit at 75d5c32 and the bell at
f56b463, reviewed on 2026-10-31 against main at 075574e. The owner's conclusion from the review is in the section
Review 2026-10-31: what is worth building is independent of a language of its own and is built in
any case; the language is deferred until it offers what a format cannot. The decisions at the end were taken the same
day: TOML with names as the form, the pin map from the parts entry, the hash over the form's canonical data, a resolved
graph.json generated beside the form, four complete variants, the symbol's names and the roles, no net in fragments;
what both forms share has a plan of its own, circuit-language-plan.md. Since the
sketch, the cut of rules at scale (4982d35) has changed the graph's keys and the codes; the examples below and the
prototype still use the old ones, and the review lists what to bring along. The Christmas bell's main configuration is
written down both ways in prototypes/circuit-language: as a language,
bell.circuit, and as TOML with pin names,
bell.toml. The translator translate.py takes all four configurations there
and back without a difference, at the state of 75d5c32.
Goal¶
Decision 8: write the whole bell down both ways, then choose which one edakit builds. The concept names four things that make the JSON cryptic: pin numbers instead of names, names that mislead (LED5's pin 1 is "C" but is the anode), a path through the circuit that falls apart, and no comments. Its touchstone: translate there and back without a difference, so that every check keeps running on today's graph.
The two forms¶
The language (.circuit) has one statement per part and per net. A net is a line with its voltage range. A path
is a few net lines in a row, with the pin lock at its pin. Comments start with #. Quoted text may run over several
lines.
TOML with names is today's graph in TOML. Components, nets and cables are tables keyed by designator and net name. It has the same fields as the JSON, with comments and multi-line strings.
Both write a pin by role or name (LED5.anode, U1.PD6) and resolve it through the parts entries. Both take a
part's pin map from its parts entry, and both keep the order of parts, nets and pins, so the JSON comes back with
the same lists in the same order.
The extension .circuit says what the file holds. SPICE netlists use .cir, .ckt and .net, and edakit writes
.cir for the simulation; KiCad netlists use .net, Logisim .circ, atopile .ato. [project] graph names the file,
so a project would have hardware/graph.circuit.
Side by side¶
The Nano¶
JSON, 65 lines in the file (pin map and nc shortened here):
{"designator": "U1", "type": "module", "label": "Arduino Nano 3 (plugged in)", "part": "Arduino Nano V3",
"lcsc": "C9900187880", "bom": false,
"pins": {"1": "TXD", "2": "RXD", "3": "RESET", "4": "GND", "5": "PD2", "…": "…", "29": "GND", "30": "VIN"},
"nc": ["1", "2", "3", "10", "12", "13", "16", "17", "18", "19", "…", "28", "30"],
"waive": {"P-1": "VIN stays free: the Nano runs from its USB socket"},
"notes": "Plugs into two 1x15 female headers, …"}
Language, 7 lines:
part U1 module "Arduino Nano V3" lcsc C9900187880 no-bom "Arduino Nano 3 (plugged in)"
nc TXD RXD RESET PD7 PB1 PB2 PB5 3V3 AREF PC0 PC1 PC2 PC3 PC4 PC5 ADC6 ADC7 PC6 VIN
waive P-1 "VIN stays free: the Nano runs from its USB socket"
notes "Plugs into two 1x15 female headers, pitch 2.54 mm (hand-soldered, not in the graph). Powered through its
USB socket; …"
TOML, 14 lines:
[components.U1]
type = "module"
label = "Arduino Nano 3 (plugged in)"
part = "Arduino Nano V3"
lcsc = "C9900187880"
bom = false
nc = ["TXD", "RXD", "RESET", "PD7", "PB1", "PB2", "PB5", "3V3", "AREF", "PC0", "PC1", "PC2", "PC3", "PC4", "PC5",
"ADC6", "ADC7", "PC6", "VIN"]
waive.P-1 = "VIN stays free: the Nano runs from its USB socket"
notes = """\
Plugs into two 1x15 female headers, pitch 2.54 mm (hand-soldered, not in the graph). Powered through its USB \
socket; …"""
In both forms the pin map comes from the parts entry C9900187880. Its two GND pins are U1.GND where they stand
together in a net. One name misleads and no role helps: the symbol swaps the labels of pins 14 and 15, so U1.PB4
is D11 (PB3 on the chip). The form cannot know; both files say it in a comment.
Step 1: D6 → 330 Ω → green LED → GND¶
JSON, 48 lines in four places of the file (shortened here):
{"designator": "LED5", "type": "led", "label": "5 mm LED, green, countdown step 1", "part": "XL-504UGD",
"lcsc": "C2895549", "bom": false, "pins": {"2": "A", "1": "C"}},
{"designator": "R4", "type": "resistor", "label": "Series resistor LED5", "part": "330 Ω 0805", "lcsc": "C17630",
"value": "330Ω", "pins": {"1": "1", "2": "2"}},
{"name": "LED_G1", "pins": ["U1.9", "R4.1"], "v": [0, 5.5]},
{"name": "LED_G1_A", "pins": ["R4.2", "LED5.1"], "v": [0, 5.5]},
{"name": "GND", "pins": ["U1.4", "U1.29", "C1.2", "J1.1", "R6.2", "R7.2", "LED5.2", "…"], "v": [0, 0]},
"pin_locks": {"U1.9": "LED_G1", "…": "…"}
Language: two lines in the parts table, and the path in one piece:
part LED5 led XL-504UGD lcsc C2895549 no-bom "5 mm LED, green, countdown step 1"
part R4 resistor "330 Ω 0805" lcsc C17630 value 330Ω "Series resistor LED5"
…
# Step 1: D6 → R4 → green LED5 → GND
net LED_G1 0..5.5 V U1.PD6 (locked) R4.1
net LED_G1_A 0..5.5 V R4.2 LED5.anode
net GND LED5.cathode
TOML: two component tables, two net tables; the cathode stays in the GND table, the lock in [checks.pin_locks]:
[components.LED5]
type = "led"
label = "5 mm LED, green, countdown step 1"
part = "XL-504UGD"
lcsc = "C2895549"
bom = false
[components.R4]
type = "resistor"
label = "Series resistor LED5"
part = "330 Ω 0805"
lcsc = "C17630"
value = "330Ω"
# Step 1: D6 → R4 → green LED5
[nets.LED_G1]
v = [0, 5.5]
pins = ["U1.PD6", "R4.1"]
[nets.LED_G1_A]
v = [0, 5.5]
pins = ["R4.2", "LED5.anode"]
# in [nets.GND]: "LED5.cathode", "LED4.cathode", …
# in [checks.pin_locks]: "U1.PD6" = "LED_G1" # D6
The misleading "C" appears in neither form: an LED is written by its roles.
A waiver¶
The bell waives on one component only, the Nano's P-1 above; none of its nets has a waiver. A net waiver as in edakit's example led-bar:
{"name": "LED_CLK", "pins": ["U1.4", "U2.3"], "v": [0, 3.4], "fn": "spi.sck", "fn_bus": "LED",
"waive": {"G-4": "Floating until the firmware runs, but harmless: R2 holds OE high, so no output can switch on."}}
net LED_CLK 0..3.4 V U1.GP2 (locked) U2.CLK
fn spi.sck fn_bus LED
waive G-4 "Floating until the firmware runs, but harmless: R2 holds OE high, so no output can switch on."
[nets.LED_CLK]
v = [0, 3.4]
pins = ["U1.GP2", "U2.CLK"]
waive.G-4 = "Floating until the firmware runs, but harmless: R2 holds OE high, so no output can switch on."
fn = "spi.sck"
fn_bus = "LED"
The servo¶
The bell has no cables entry: the servo brings its own cable and plugs into J1 (hint H-7 of the variants). In the
graph it is J1, the net SERVO and J1's pins in +5V and GND. Language:
part J1 connector PZ254V-11-03P lcsc C2937625 no-bom "Servo (GND, +5 V, signal)"
notes "Standard servo plug: brown = GND (1), red = +5 V (2), orange = signal (3)."
…
# Servo on D8. Its own cable plugs into J1: brown GND, red +5 V, orange signal. C1 takes its start-up peaks.
net +5V C1.plus J1.2
net GND C1.minus J1.1
net SERVO 0..5.5 V U1.PB0 (locked) J1.3
In TOML, J1 is a table like LED5 above, SERVO is pins = ["U1.PB0", "J1.3"], and J1's other pins stand in the
arrays of +5V and GND, each with a comment. J1's pins stay numbers: its roles pin1 … pin3 say no more. A real
cable, as in handbook 3.1, would be cable W1 J2 J1 1=1 2=2 r_ohm_per_wire 0.065 in the language and a table
[cables.W1] in TOML.
purpose¶
"purpose": {"use": "private", "market": "EU",
"notes": "Automatic Christmas bell for the owner's home, built and used by the owner – not sold or given away (D-16). Powered from a USB port or power bank through the Nano's USB; no battery holder, no mains, no radio."}
purpose private market EU
notes "Automatic Christmas bell for the owner's home, built and used by the owner – not sold or given away (D-16).
Powered from a USB port or power bank through the Nano's USB; no battery holder, no mains, no radio."
[purpose]
use = "private"
market = "EU"
notes = """\
Automatic Christmas bell for the owner's home, built and used by the owner – not sold or given away (D-16). \
Powered from a USB port or power bank through the Nano's USB; no battery holder, no mains, no radio."""
The content is the same in all three. The regulatory review M-6 reads purpose whole, as today (KEEP_WHOLE in
gate.py), and it stays in the circuit hash under either hash rule below. M-6 is support, not legal advice: nothing
in it is legally guaranteed, and it makes no claim to being correct.
Every field of the graph¶
| Field | Language | TOML with names |
|---|---|---|
name, version, date |
graph christmas-bell version 0.3 date 2026-10-25 |
name = "christmas-bell", … |
description |
description "…" |
description = """…""" |
purpose |
purpose private market EU and notes "…" |
[purpose] |
conventions, hints |
convention v "…", hint H-1 "…" |
[conventions], [hints] |
ground, grounds |
ground GND, grounds AGND |
ground = "GND", grounds = ["AGND"] |
component: designator, type, part |
part R4 resistor "330 Ω 0805"; - for a missing one |
[components.R4], type, part |
lcsc, value, board, supplier, spare |
lcsc C17630 value 330Ω board main supplier rs spare 1 |
keys of the table |
bom: false |
no-bom |
bom = false |
label |
the quoted text at the end of the part's line, or label "…" |
label = "…" |
pins (pin map) |
from the parts entry; pins 1=TXD 2=RXD … only where the graph differs |
the same; pins = {…} |
nc |
nc TXD RXD …, pins by name or role |
nc = ["TXD", "RXD", …] |
waive |
waive P-1 "…", one line each |
waive.P-1 = "…" |
verify, notes |
verify "…", notes "…" |
keys |
net: name, v |
net LED_G1 0..5.5 V; 0 V for [0, 0] |
[nets.LED_G1], v = [0, 5.5] |
net: pins |
the words after the range; a later net GND LED5.cathode adds pins |
pins = ["U1.PD6", "R4.1"] |
fn, fn_bus, bus, pwm_hz |
fn spi.sck fn_bus LED |
keys |
cables |
cable W1 J2 J1 1=1 2=2 r_ohm_per_wire 0.065 |
[cables.W1] |
layout |
layout fixed …, layout edge J1, layout near R1 U2, layout loop C5.1 U3.2 max_mm 8 "…", layout note "…" |
[layout]; loop ends by name |
checks.pin_locks |
(locked) after the pin in its net; lock PIN NET for a pin on another net |
[checks.pin_locks], keys by name |
checks.reverse_protect |
reverse_protect J1 |
reverse_protect = ["J1"] |
| anything else | set KEY JSON, e.g. set easyeda_device {…}, set checks.doc_pin_table {…} |
written as TOML |
Rules the round trip needs¶
| Case | Example | Rule |
|---|---|---|
| A pin has a role of its own | LED5 pin 1: name "C", role anode |
The role first, except on ICs and modules: LED5.anode. The misleading name never appears. |
| A name of one pin | Nano pin 9: PD6 | U1.PD6; on ICs and modules the name comes first, their names are the datasheet's. |
| Pins share a name | Nano pins 4 and 29: GND | The name stands for all of them, in the entry's order, where they follow each other in a list: U1.GND. Elsewhere the number: U1.4. |
| Pins share a name, one is used | T-Display pins 12 and 13: 3V3, 13 not connected | The number: U1.12, nc 13. |
| Pins share a role | a button's pins 1 and 2: role a |
As a shared name: S1.a stands for both. |
| A word that says no more than the number | resistor pin "1", connector role pin3 |
The number: R4.1, J1.3. |
| A name that is no word | the Pico's TP1, "GND (USB)" | The number: U1.TP1. |
| A name misleads and no role helps | Nano pin 14: "PB4", really D11 = PB3 | U1.PB4, and a comment. The translator cannot know. |
| Reading a word | The pin number first, then the name of one pin, then the role of one pin, then a name or role several pins share. The writer takes the first word that reads back as exactly the pins it means, else the number. | |
| Pin map | From the parts entry; written only where the graph's differs. In all four configurations it equals the entry. | |
| Order | Parts, nets and the pins of a net come back in the order written. Free arrangement is only possible within that order: bell.circuit puts the parts first and then the wiring in blocks. |
|
| A net over several lines (language) | net GND LED5.cathode |
A later line adds pins, and attributes the net does not have yet; a range or attribute twice is an error. |
| Pin locks | U1.PD6 (locked) |
At the pin in its net. A lock on a pin that sits on another net (a G-2 finding): lock PIN NET. |
| A value without a form of its own | easyeda_device, the empty cables |
set KEY JSON in the language (set cables []). TOML holds every JSON value as it is. |
| null | TOML has none; the TOML writer refuses a graph with one. None of the graphs has one. | |
| A broken graph | two parts R4, a pin U1.99 |
Both writers refuse it with the reason: G-1 reports it; fix the JSON first. |
| The wrapper | {"graph": {…}} |
The forms hold the inner graph; the JSON comes back with the wrapper, as every edakit project has it. |
| Text over several lines | Language: a line break inside quotes reads as one space. TOML: a line-ending backslash in """…""". |
The round trip¶
test_translate.py runs graph JSON → form → JSON for both forms and compares canonical JSON (keys sorted, every list
in its order). BELL_ROOT=<checkout of the bell> python3 test_translate.py; without BELL_ROOT the bell's cases are
skipped, and edakit's pytest does not collect the folder.
| Case | JSON | TOML | Language | Result |
|---|---|---|---|---|
| bell, main | 515 | 240 | 63 | equal, both forms |
| bell, smd | 518 | 245 | 73 | equal, both forms |
| bell, tht | 531 | 256 | 68 | equal, both forms |
| bell, esp32 | 519 | 265 | 87 | equal, both forms |
| led-bar, edakit's example | 610 | 252 | 75 | equal, both forms |
| led-bar with every field the bell does not use | 696 | 304 | 102 | equal, both forms |
bell.circuit, bell.toml, written by hand |
515 | 281 | 114 | equal to the bell's hardware/graph.json |
The numbers are lines. In the first six rows the forms are what the translator writes. The sixth case adds cables,
layout loops, ground and grounds, hints, an own pin map, a part without parts entry, a lock off its net, values
that are no strings, quotes, backslashes and line breaks.
Line counts¶
| The bell, main configuration | Lines | Without comments and blank lines | Bytes |
|---|---|---|---|
hardware/graph.json |
515 | 515 | 9 294 |
| TOML, written by the translator | 240 | 201 | 5 829 |
TOML by hand, bell.toml |
281 | 211 | 8 283 |
| Language, written by the translator | 63 | 59 | 4 422 |
Language by hand, bell.circuit |
114 | 72 | 7 608 |
The JSON's length is mostly line breaks: one value per line. In bytes, which is what Claude reads, the translator's language takes about half of the JSON and its TOML about two thirds. Comments and arrangement make the hand-written language 72 percent larger than the translator's, the hand-written TOML 42 percent.
The variants¶
Today the bell has four complete graphs, each a project of its own (variants/<v>/edakit.toml) with its own parts,
simulation, boards and sign-offs. They differ in parts and texts; the wiring is mostly the same. Changed lines against
main, as the translator writes them:
| Variant | JSON | TOML | Language | Its nets |
|---|---|---|---|---|
| smd | 117 | 83 | 40 | JSON: GND, LED_Y1_A and LED_Y2_A differ. Both forms: only GND, the order of R8 and R9 |
| tht | 92 | 74 | 37 | identical in all three |
| esp32 | 350 | 225 | 116 | all differ: another module, another supply |
Roles do this. The 0805 LEDs of smd have the anode on pin 1, the yellow 5 mm LEDs of main on pin 2: two pin numbers in
JSON, one LED3.anode in both forms.
A variant could also list only its differences: use ../../hardware/graph.circuit and the statements that replace
the main file's, by designator and net name. SMD would be 26 lines of content instead of 69, THT 26 instead of 64;
ESP32 changes 37 of its 45 statements and gains nothing. TOML has no include; edakit would define one (base = "…"
and tables replaced by key). Either way the variant's circuit then depends on the main file: a change there changes
the variant's hash and voids its sign-offs, where today the four are independent. Not built in the prototype.
What each form costs¶
Parser and formatter¶
The prototype, translate.py, in lines of code without comments and blank lines:
| Part | Language | TOML with names |
|---|---|---|
| Pins by role or name, shared | 89, and 33 of helpers | the same |
| Reading | 242: scanner, statements, every field, errors with lines | tomllib from the standard library, and 35 to resolve names |
| Writing from the graph (moving a project, the tests) | 166 | 99: the standard library has no TOML writer |
| A formatter that keeps comments and arrangement | not built; needed for edakit graph fmt |
none needed for reading; tools that edit need a comment-keeping editor (tomlkit, a dependency) or a line editor of their own |
Built into edakit with validation per statement, tests and messages, the language may come to two or three times that. TOML stays near its prototype.
Error messages¶
| Language | TOML with names | |
|---|---|---|
| A typo in a pin | line 93: 'LED5.anod': no pin of that number, name or role on LED5 |
line 233: … the same, the line found by searching the text for the word |
| A syntax error | with its line | with line and column (tomllib) |
| A wrong shape | 0..5,5 V: line 93: net LED_G1_A: '0..5,5 V' is no voltage range |
v = [0, 5,5] is valid TOML, three numbers; G-1 reports it later with the net's name, no line |
| A range twice | line 110: net GND: v given twice |
a key twice: a TOML error |
Today a JSON syntax error has its line and column, and G-1 names nets and pins, not lines. Both forms keep that for the checks.
Handbook¶
The language needs a chapter with its syntax and rules, read before every edit of the graph: the skills
circuit-design and hardware-change point to it, and the reviewers read the graph. About a page; it costs context
each time, against the half of the JSON's bytes the language saves. TOML needs a section in chapter 3: tables by
key, pins by role or name. Claude and every editor know TOML; GitHub and editors highlight it. Nothing highlights
.circuit.
Tools that write the graph¶
| Writer | Today | Language | TOML with names |
|---|---|---|---|
edakit block add (block.py) |
reads and writes the JSON itself | appends the block's statements: its parts, its nets, net GND C5.minus for pins on an existing net, bus i2c in such a line |
new tables at the end; pins and attributes for existing nets go into their tables: an editor that keeps comments |
New: edakit graph convert, edakit graph json |
– | convert a project once (the translator's writer); print the graph as JSON | the same |
New: edakit graph fmt |
– | spacing and columns, keeping comments and arrangement | not needed |
| Skills | circuit-design (§5), hardware-change (§1), own-part (§4), new-project (the empty hardware/graph.json) |
examples and wording to the language | to TOML; smaller changes, the structure stays |
| Agents | electrical-reviewer, regulatory-reviewer, project-surveyor, requirements-engineer read the graph | read the chapter | read TOML |
| Handbook | chapters 3, 7, 10, 14 and 15 show the graph's JSON or its path | to the language | to TOML |
load_graph() (design.py) |
reads JSON | reads .circuit by its extension; JSON stays |
reads .toml by its extension; JSON stays |
No key, no change: a project keeps hardware/graph.json until it converts. The bell's own mentions of graph.json
(hardware/README.md, docs/decisions.md, a comment in the esp32 firmware) change in the bell, at the user's request.
edakit check and the hashes¶
The checks keep running on today's graph, in both forms. load_graph() returns the same dict as from the JSON;
every check, the simulation, the schematic, the hand-over and the firmware contract read it unchanged. Two things
move:
- P-1 compares the graph's pins with the parts entry. Where the form takes the pin map from the entry, that half has
nothing left to compare. The comparison with the library's symbol from
part importstays. - Findings name pins by number (
U1.9 (PD6) is neither connected nor nc). They should name them as the form does; the writer's rule gives the word.
The circuit hash. Today it covers graph.json as bytes, or as canonical JSON without the keys of
[release] hash_ignore. content() in gate.py keeps a file that is no JSON as bytes, so hash_ignore would stop
reaching into the graph without a word. Two ways, the same for both forms:
- the translated graph as canonical JSON: comments, line breaks and the arrangement count for nothing, every change
of data does, and
hash_ignoreworks as today; - the bytes of the form: every comment and every rearrangement voids M-1 and M-6.
graph_sha256 in the gate protocol names the form's bytes either way. The parts entries are in the hash already, and
the names resolve through them.
A rewire through a parts entry. A corrected role moves the connection without a change in the form: if LED5's
anode were found on pad 2, LED5.anode would now mean pin 2. Today the graph keeps LED5.1, and the simulation
reports a reversed LED until someone changes the graph. With a form the board follows the entry. The hash sees the
entry's change and M-1 lapses; the form's diff does not show it, edakit graph json before and after does.
Recommendation¶
The recommendation of the sketch, 2026-10-29. The review below weighs the same facts differently, and the owner followed the review: the language is deferred.
Build the language.
- It is the only form that fixes all four points of the concept. TOML fixes three: its paths keep the cathode in
GND and the lock in
[checks.pin_locks]. - The cost is medium. The prototype reads every field in 242 lines and writes it in 166. Pins by role or name,
load_graph()and the hash rule are needed by both forms and can be built first. - Tools write it by appending.
block addappends statements; TOML needs an editor that keeps comments, or a dependency. - Every error has its line, and the parser checks shapes (a range is two numbers) before any check runs.
- It is short where review happens: a part is a line, a net is a line, and the bell's circuit is 72 lines of content against 211 in TOML.
Against it: a syntax nobody knows yet, read from the handbook before every edit; nothing highlights it; a net may be
spread over several lines (GND over nine in bell.circuit); and a parser to maintain. If that weighs more, TOML with
names keeps most of the gain, and the shared part is the same.
Order, if decided so: pins by role or name with load_graph(), the hash rule and edakit graph json first (both
forms need them, size M); then the parser, edakit graph convert and fmt, the chapter and the skills (size M); the
bell converts last, in its repository.
Review 2026-10-31¶
Reviewed against main at 075574e, with the prototype run on edakit's led-bar; later the same day the bell's four
configurations at its 059162a as well – equal in both forms, and bell.circuit and bell.toml equal to the main
graph, so the tables above stand. The
owner's conclusion, in their words: the single ideas are worth it, and they are all independent of a language to be
created; the language itself gives no visible gain today beyond the wish to have one. Two qualifications the owner
took along: a path as a block is the one thing only a language does well, and the language's gain is small, not nil.
Against the yardstick: nothing in the sketch leaves anything only in EasyEDA or only in someone's head. What moves is where a pin number is decided, from the graph to the parts entry – a matter of what a diff shows, not of text.
What held¶
- The round trip on today's led-bar is equal in both forms, 76 lines as a language and 250 as TOML; the line counts
of the prototype above are right;
content()keeps a file that is no JSON as bytes, sohash_ignorewould stop reaching into a.circuitor.tomlgraph without a word;graph_sha256takes the file's bytes. - The sizes, read as what Claude reads: the bell's language by hand is 7 608 bytes against 9 294 of the JSON, 18 percent less; the TOML by hand 11 percent less. The translator's 52 percent hold for a file without comments, which nobody reads. The argument the other way is as weak: a handbook page for the language costs little next to chapter 3, which circuit-design and hardware-change read already and which is 52 KB with its generated code list. Context decides nothing here; readability and the error rate of an edit do.
Stale since 75d5c32¶
- The graph's keys:
waiveisacceptkeyed by codes,hintsisaccepton the graph,checks.pin_locksandchecks.doc_pin_tablearelocks.pinsandlocks.doc_pin_table,checks.reverse_protectissource = "reversible"on the component;old_form()indesign.pyrefuses the old keys. The field table, the examples,bell.circuit(waive P-1) andbell.toml([checks.pin_locks]) still write them;test_translate.pystops on today's led-bar withKeyError: 'checks'. The translator still runs, but writes the new keys asset accept {…}on every net,set source "reversible"andset locks {…}with pin numbers in place of(locked). - The codes: P-1 is STRUCT.entry; G-1 is STRUCT.name, STRUCT.pin, STRUCT.net, STRUCT.board, STRUCT.ref and STRUCT.purpose; G-2 is STRUCT.lock, G-4 is LVL.undefined, S-2 is SYNC.schematic. M-1 and M-6 stay.
- The bell's graphs are not converted: at
059162athey still carrywaive,checksandhints. The plan calls building the language size L, this sketch two steps of size M.
Findings on the rules¶
- A shared name is written only for a run of all pins of that name. The Pico of led-bar has eight GND pins, and AGND
(pin 33) stands between them in the GND net, so the writer gives nine numbers:
U1.3 U1.8 … U1.AGND U1.38 U1.42. The cause is the touchstone's "every list in its order", although the order of pins in a net means nothing to any check. Pins in a net are compared sorted, andU1.GNDmeans all GND pins wherever they stand. - Under hash rule 1, "the arrangement counts for nothing" holds for comments and line breaks, not for a part or a net
moved:
part R1andpart R2swapped give another canonical JSON. Today's JSON hash has the same property. The canonical form sorts components by designator, nets by name and the pins of a net, so that arrangement counts for nothing; the only order the engines read is the name of a net joined by bridges, after the first of them in the graph, which is cosmetic. - A name that is no word can only be written as its number:
U1."GND (USB)"is refused, quotes read only inpinslines, and a#in a name starts a comment. That leaves out the pins a name helps most with, active-low ones such asRST#orCS#. Quoted references, or#as a comment only after white space. - The TOML form was given the JSON's structure as it is.
[[nets]]as an array of tables may repeat a name, and edakit joins the fragments as a laternet GND …line does: a path as a block in TOML too, three lines a fragment. Point 1 of the recommendation, "the only form that fixes all four points", does not hold against that form. - A lock stays a statement of its own.
(locked)in the net, or a marker in the pin word, travels with the pin when an edit moves it, and STRUCT.lock has nothing left to find;locks.pinsnames the net the firmware expects apart from where the pin stands, which is what the check needs. The lock elsewhere is therefore no part of the third of the four points; only the cathode in GND is. - Loading hangs on the parts directory:
load_graph()needs the entries, and a part without an entry and without apinsline is a parse error, where today a missing entry is a STRUCT.entry finding with everything else still checked. The reader should keep such a part's pins by number and leave the finding to the check. - A rewire through a parts entry means the graph is no longer the only place where connections are decided
(handbook 3). Decision 4's second option, a generated
graph.jsoncommitted and checked in the pre-commit hook, is the pattern offirmware.jsonandstock.json; "two files change with every edit" is true of the firmware contract today. M-1 reads diffs, so that option weighs more than the sketch gave it. - Under hash rule 1 a
#comment never reaches the hash: regulatory statements stay inpurpose.notes, whichKEEP_WHOLEreads whole for M-6 – support, not legal advice. - Converting a project changes the circuit hash.
carry_signoffsinconvert.pyand the gate'srehashedentry carry the sign-offs over when the files were the signed ones; the conversion does the same. edakit graph fmtis needed by no step; the KiCad tool chain reads the Design, not the file; the conventions textpin_referenceof a converted project is rewritten, asconvert.pyrewrites thewaiveconvention today.
Built in any case¶
Independent of the form, and of the language:
- Pins by name or role,
U1.PD6,LED5.anode, resolved through the parts entries with the reading rule of the table above: the number first, then the name of one pin, then the role of one pin, then a name or role several pins share, which stands for all of them. This is the correctness gain of the whole sketch: a model knows PD6 and anode from the datasheet and the firmware, while a symbol pin number is looked up, which is where wrong pins and reversed LEDs come from; and it makes the variants alike.pins_by_roleandpin_infoof the Design class are where it goes; findings name pins by the same rule. - The pin map from the parts entry, written only where the graph's differs; a part without an entry keeps its pins by number, and STRUCT.entry reports it (decision 3).
- The circuit hash over the data of the form as canonical JSON,
hash_ignoreapplied, names unresolved – the entries are in the hash already, so a rewire through an entry changes it –, components, nets and the pins of a net sorted (decision 2). Comments and arrangement count for nothing; regulatory text stays inpurpose.notes. edakit graph jsonprints the resolved graph with pin numbers, which is where a rewire through an entry shows;edakit graph convertmoves a project once and carries its sign-offs over asedakit convertdoes.- The lock stays
locks.pins(finding 5).
What remains of decision 1 is the form: TOML with names, or names and roles in today's JSON. TOML gives comments, a
third of the lines and a format every model and editor knows, at the price of a line editor for block add as
write_place_rows is one for [place]; JSON gives no new format and no comments.
Hard without a language¶
What only a language does well; none of it is a correctness gain:
- a path as a block: a net written in pieces where the circuit goes,
net GND LED5.cathodeafter the LED's step – in TOML only as[[nets]]fragments of three lines each, in JSON not at all; - a part and a net as one line of bare words,
U1.PD6 R4.1instead of["U1.PD6", "R4.1"]: the bell's circuit in 72 lines of content against 211 in TOML; - a shape error with its line before any check:
0..5,5 Vis refused at its line,v = [0, 5,5]is valid TOML and a STRUCT.net finding without one; block addby appending statements, where TOML needs an editor that keeps comments and arrays.
What a language would need to be worth it¶
A language earns its cost through what a format cannot give. The sketch's language is a serialisation of the graph with a nicer syntax, so it is a second format for the same data. It would be worth building when it has:
- Abstraction. A building block as a statement,
use led STATUS in=GP8_LED gnd=GND, expanded when the graph is loaded instead of copied in byblock add– a change to the block then reaches every use, with what that does to the hash and the sign-offs written down; repetition, eight LEDs onOUT0…OUT7as one statement; values with their unit checked when read,330Ω,0..5.5 V; and variants as differences,use ../../hardware/graph.circuitwith the statements that replace the main file's, with a hash model that says what a change of the main file does to a variant's sign-offs (decision 5). - A schema that stands still. Every key of the graph has a statement and
set KEY JSONis not needed. Since75d5c32the graph gainedaccept,locks,source,instrument,load_min_ohm,matrixandpurpose.radio; in a format a new key is free, in a language each is a syntax decision. - Evidence.
evals/add-ledin the language and in the form, three runs each with Sonnet (maker-coverage FT-24): the language is edited at least as reliably. - The tooling a format gets for free, as preconditions rather than costs: highlighting on GitHub and in editors, a formatter, line-exact errors, one reader every tool uses and one writer for everything that writes the graph.
Thought further on the owner's request in circuit-dsl.md: whether a fitting language exists already, what one of edakit's own would have to be able to do, on which basis, and when.
Decisions¶
Taken on 2026-10-31 in a decision round after the review. "Accepted" is the owner's yes to the recommendation; "decided by Claude" marks what the owner left to Claude and confirmed. The language itself is deferred – the owner's conclusion of the review – until it offers what a format cannot (the section Review 2026-10-31 says what that is).
| # | Question | Decision | Status |
|---|---|---|---|
| 1 | Which form edakit builds for the circuit | TOML with names, hardware/graph.toml: comments, a third of the lines, a format every model knows and GitHub highlights; block add gets a line editor that keeps comments and arrays, as write_place_rows is one for [place]; names and roles in today's JSON was the alternative |
accepted |
| 2 | What the circuit hash covers | the data of the form as canonical JSON, hash_ignore applied, names unresolved (the parts entries are in the hash already), components by designator, nets by name and the pins of a net sorted: comments and arrangement count for nothing, every change of data does; regulatory text stays in purpose.notes |
accepted |
| 3 | Where a part's pin map comes from | from its parts entry; the form states it only where the graph differs; a part without an entry keeps its pins by number, and STRUCT.entry reports it instead of a loading error | accepted |
| 4 | A resolved JSON beside the form | yes: generated, committed and checked in the pre-commit hook as firmware.json is, so that every rewire through a parts entry shows as pin numbers in the diff M-1 reads; two files change with every edit |
accepted |
| 5 | How the variants are written | four complete files, as today; a variant that names the main file and lists its differences is something a later language would have to offer with its hash model | accepted |
| 6 | Which pin names a form may use | the symbol's names and the roles, as STRUCT.entry and SYNC.schematic hold them; the Nano's swapped labels get a comment; no alias per pin | accepted |
| 7 | A net in fragments (review, finding 4) | no: a net is one table, [nets.GND] once with all its pins, one way to write a net and one place a reviewer reads it; a path as a block stays the task of a later language |
accepted |
Decided by Claude, confirmed: (1) a pin word is read as the number first, then the name of one pin, then the role of
one pin, then a name or role several pins share, which stands for all of them; the writer takes the first word that
reads back as exactly the pins it means, else the number; findings name pins by the same rule. (2) The pins of a net
are compared sorted; U1.GND means all GND pins wherever they stand. (3) Doubled names and unknown pins are refused
when the graph is loaded, not translated; a part without an entry keeps its numbers for STRUCT.entry to report.
(4) TOML keys components, nets and cables by designator and name and has no null; locks.pins stays a table of its
own. (5) The source is hardware/graph.toml, named by [project] graph; the generated, resolved file keeps the name
hardware/graph.json, with pin numbers and the wrapper {"graph": …}, so that project scripts that read the graph as
JSON keep running. (6) edakit graph convert translates a project once, carries its sign-offs over as edakit convert
does and rewrites the convention pin_reference; edakit graph json prints the resolved graph; edakit graph fmt is
dropped. Deferred with the language: the extension .circuit, (locked) and lock PIN NET, set KEY JSON, a later
net line that adds pins, quoted text over several lines.
What both forms share has a plan of its own, circuit-language-plan.md: nine steps,
built with add-feature when the owner asks for them. The prototypes of this folder stay the record of the sketch at
75d5c32; the round trip they ran moves into the plan's tests.
Sources¶
- edakit at
75d5c32: design.py (load_graph,entry_key), checks.py (check_structure, G-1), gate.py (content,state_files,state_hash,KEEP_WHOLE,header), block.py (add,plan,write_json), scripts.py (the data namegraph), sim/run.py (.cir), handbook 3 and 4 (pins, roles), the skills circuit-design, hardware-change, own-part and new-project, the exampletests/fixtures/led-bar. - The Christmas bell at
f56b463:hardware/graph.jsonandhardware/parts/,variants/{smd,tht,esp32}/hardware/with theirparts/,src/firmware/buttons.ino(the Arduino pins D2 … D12). - The prototype: prototypes/circuit-language –
translate.py,test_translate.py,bell.circuit,bell.toml.