Design file format
The native .design.* container, its write-append list, schemas, merge rules and the KiCad mapping.
The native file format of SoftForge Electronics Designer: .design.* files, format version 1.0. This page is the
reference for people and programs that read or write them by hand, from scripts or from agents. The library that
implements it is src/design/; the JSON schemas are in schemas/design/; the command line is edsnx design.
The editors open design files and keep a design cache next to the KiCad files of a KiCad project (§19); KiCad files
become design files and back with the KiCad importer and exporter (§18, edsnx design import | export | migrate).
Everything on this page is stable for format 1.0.
1. Overview#
A design file is one JSON object with two parts:
tree, the document as it was after event numbertree.seq, consolidated;wal, the write-append list: one event per line, every change made since.
The document is tree with every wal event applied in order. An edit appends a line at the end of the file and
never rewrites the rest, so saving is cheap on any file size, two branches that edit the same file merge line by
line, and the history of a design is readable in the file itself. From time to time the list is consolidated:
folded into the tree, the list restarted.
{
"designVersion": "1.0",
"kind": "pcb",
"$schema": "https://soft-forge.dev/schemas/design-1.0/pcb.schema.json",
"$comment": "SoftForge Electronics Designer design file, format 1.0. …",
"tree": {
"seq": 0,
"meta": { "createdAt": "2026-10-18T09:00:00.000Z", "id": "…", "name": "blinky", "updatedAt": "…" },
"attachments": [],
"baselines": [],
"comments": [],
"document": { "bounds": { "max": [50, 35], "min": [0, 0] }, "items": [], "kind": "pcb", "layers": [], "nets": [], "primitives": [], "title": "blinky" },
"sections": {}
},
"wal": [
{"seq":0,"op":"wal.base"}
,{"seq":1,"at":"2026-10-18T09:00:05.000Z","by":"Ada","op":"addNet","args":{"name":"GND"}}
,{"seq":2,"at":"2026-10-18T09:00:10.000Z","by":"Ada","op":"addTrack","args":{"layer":"F.Cu","net":"GND","pts":[[10,10],[20,10]],"width":0.25},"ids":["02dc09a9-ca7e-89fd-899e-5b97b17548e5"]}
]
}2. Files and the container#
One file per kind; the kind is the last part of the extension.
| Extension | Kind | Tree holds |
|---|---|---|
.design.prj | prj | the project: documents, sheet hierarchy, library tables, net classes and rules, settings, project sections, reviews |
.design.sch | sch | one schematic sheet (renderer document, sections) |
.design.pcb | pcb | one board (renderer document, board sections: flex, SI, MCAD, rooms, test points, references to requirements and panels) |
.design.sym | sym | a symbol library |
.design.fp | fp | one footprint |
.design.doc | doc | a documentation sheet (views, tables, balloons, dimensions, notes) |
.design.panel | panel | a panel (boards, layout, rails, separation) |
.design.req | req | the requirements of a board (families, requirements, results, history) |
Members of the container, in the order they are written:
| Member | Type | Meaning |
|---|---|---|
designVersion | string | Format version, numbers separated by dots (§14). Required. |
kind | string | One of the kinds above. Required; a reader falls back to the file name when it is missing. |
$schema | string | The JSON schema of the kind, written on every save, ignored by the reader. |
$comment | string | A fixed note for people reading the raw file, written on every save, ignored by the reader. |
| other | any | Unknown members (for example x-… extensions of other tools) are kept and written back, sorted. |
tree | object | The consolidated document (§4). |
wal | array | The wal.base marker, then the events (§6). |
Layout on disk#
The Electronics Designer writes every file in one canonical layout, so that the same document always gives the same bytes and a git diff shows exactly what changed:
- UTF-8,
\nline ends, a final line end; two-space indentation; - the container members in the order above; in
tree,seqandmetafirst, every other key sorted; every object key below that sorted (code-unit order); - short arrays of numbers, strings and points (at most 100 characters) on one line,
[[10, 10], [20, 10]]; walwith one entry per line: the marker first, every event after it on its own line starting with a comma (,{"seq":…}), so an append adds a line and touches no other;- numbers canonical (§12).
A file in another layout reads the same; the next save writes it canonical. edsnx design validate reports a file
that is not in the canonical layout as a warning.
3. The document#
To compute the document, take tree and apply every event of wal whose seq is above tree.seq, in list order.
An event that cannot be applied (an unknown operation, an object that does not exist) is skipped with a warning
and stays in the list; after every event, applied or skipped, the working tree.seq becomes the event’s seq and
meta.updatedAt its at when later.
The design state of a tree is the tree without seq, baselines, comments and the bookkeeping members of
meta (updatedAt, consolidations, imports, agentSessions, upgrades). Its state hash is the sha256 of its
canonical JSON (§12). Consolidation keeps the design state; baselines record its hash.
4. Trees#
Every tree has:
| Member | Type | Meaning |
|---|---|---|
seq | integer ≥ 0 | The tree is the document after this event. |
meta | object | id (UUID of the document; other files refer to it), name, description, createdAt, updatedAt, generator, project (reference to the .design.prj), kicad (cache or export, §19.1), and the bookkeeping lists imports, consolidations, upgrades, agentSessions kept by their events. |
comments | array | Comment threads (id, number, author, at, body, anchor, replies, state). |
baselines | array | Named states (§10). |
attachments | array | Files the design refers to (§13). |
The members of each kind:
- Graphic kinds (
sch,pcb,sym,fp):document, the renderer model (kindschematic/pcb/symbol/footprint,title,bounds,layers,primitives,items, and for boardsnets,setup,stackup);sections, the parts of the design that are not drawn (board:flex,si,mcad,idx,rooms,testPoints,requirements,panels; schematic:docs). Items carryid,kind,label,atand their attributes; primitives carrytype,layerand theitemIdof the item they draw. - Project (
prj):documents(id,kind,pathrelative to the project,name,root),hierarchy(sheet instances:id,docId,parent,name,page),libraries(symbolandfootprinttables:name,uri,type,options,description),rules(netClasses,diffPairs,matchedGroups,severities,custom),settings,sections(variants, blocks, output jobs, release, DFM, parts, system),reviews. - Documentation (
doc):doc(sheet, board, paper, views, tables, balloons, dimensions, notes). - Panel (
panel):panel(name, size, boards, layout, instances, rails, separation). - Requirements (
req):requirements(board, families, requirements, results, history).
KiCad source. A graphic tree imported from KiCad holds the KiCad file mapped node by node (§18): board,
schematic, library or footprint, with the extension x-kicad (format: kicad_pcb, kicad_sch, kicad_sym,
kicad_mod; file, the KiCad file name; lead and tail, text before and after the root list when there is any).
Edit intents on such a tree run through the editors’ reducers on the text the exporter prints from the mapping,
the result is mapped again and document is reduced from it; tree.set and friends may edit the mapping directly
(/board/items/12/descr) and document follows, but cannot touch /document. A tree without a mapping is native:
intents edit document directly (§6.2). Files of the first 1.0 builds kept the whole KiCad text in x-kicad.text;
the reader maps it on load.
5. Identifiers and references#
Objects have UUIDs (id). An event that creates objects lists their ids, in creation order, in ids; applying the
same list twice gives the same document. When an event names no ids, the ids are derived from the event: a UUID-shaped
value from the sha256 of the event’s content without seq and ids (or its seed member when it has one), so a
replay creates the same ids. Ids never depend on seq, which merges renumber.
Files refer to each other by meta.id, with the relative path as a hint: { "id": "…", "path": "blinky.design.pcb", "kind": "pcb" }. A program that finds no file at the path looks for a design file with that id.
6. Events#
6.1 Envelope#
| Member | Type | Meaning |
|---|---|---|
seq | integer | Strictly increasing; the first event follows tree.seq. |
at | string | ISO 8601 time with a zone (2026-10-18T09:00:05.000Z). |
by | string | Author: a user name, an agent name or a tool. |
op | string | The operation: camelCase for an edit intent, dotted for a file operation. |
| payload | The members of the operation (§6.2, §6.3). |
The first entry of the list is the marker {"seq": <tree.seq>, "at": <time of the last consolidation>, "op": "wal.base"}; it has no by and is never applied.
6.2 Edit intents#
An edit intent is the same edit the editors, the command palette, edsnx and agents make (the EditIntent ops; the
edit commands of docs/commands.md). The event carries the intent’s fields, without op, in args, and the ids it
created in ids:
{"seq":7,"at":"2026-10-18T09:00:35.000Z","by":"Ada","op":"move","args":{"by":[2.54,0],"ids":["bb71ee1c-79f2-8610-b41a-577f6d895acd"]}}On a native tree, the intents that a document of items and primitives can express by itself are applied directly:
move, rotate, delete, duplicate, lock, fields and attributes, new tracks, arcs, vias, zones, graphics, texts and nets
on a board, wires, junctions, no-connects, labels and texts on a schematic, board setup, net classes, differential
pairs, matched groups, groups and batches. The others (placing a footprint from a library, pad stacks, zone fills,
netlist updates, the library editors’ intents) need a tree with a KiCad source in format 1.0 and are skipped on a
native tree. On a project tree: setNetClasses, setDiffPairs, setMatchedGroups, setProjectSection and
batch. The payload schema of every intent is in events.schema.json ($defs/intent.<op>).
6.3 File operations#
| Operation | What | Payload | Effect | Example line |
|---|---|---|---|---|
wal.base | List marker | — | First entry of every list: repeats tree.seq; at is the time of the last consolidation. Never applied, has no by. | {"seq":7,"at":"2026-10-18T09:00:00Z","op":"wal.base"} |
meta.set | Set document metadata | set | Merges set into tree.meta (name, description, project, generator, x-*); null removes a member. id, createdAt and the bookkeeping lists cannot be set. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"meta.set","set":{"name":"Blinky rev B"}} |
import.run | Record an import | importer, version?, sources?, lost?, warnings?, note? | Appends to meta.imports: the importer id and version, the source files with their sha256, the number of objects that did not map (loss report) and of warnings. The importer writes the tree itself; the event carries no content. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"import.run","importer":"kicad","version":"10.0","sources":[{"path":"blinky.kicad_pcb","sha256":"0000000000000000000000000000000000000000000000000000000000000000","format":"kicad_pcb"}],"lost":0} |
consolidate | Record a consolidation | from, to, events | Written by consolidation right after the new wal.base marker: the events from–to (events of them) were folded into the tree. Appends to meta.consolidations. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"consolidate","from":1,"to":42,"events":42} |
format.upgrade | Record a format upgrade | from, to, title? | Written by the upgrade chain: the file was upgraded from from to to. Appends to meta.upgrades. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"format.upgrade","from":"1.0","to":"1.1","title":"Example step"} |
baseline.create | Create a baseline | baseline | Names the state after this event: appends to tree.baselines with createdAt/by of the event, seq the event’s and hash the sha256 of the design state. The event does not carry the state. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"baseline.create","baseline":{"id":"b8f4e8a0-5d1c-4c47-9a49-3a3f3c6c1f20","name":"Sent to fab"}} |
baseline.delete | Delete a baseline | id | Removes a baseline (its snapshot file is left alone). | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"baseline.delete","id":"b8f4e8a0-5d1c-4c47-9a49-3a3f3c6c1f20"} |
comment.add | Comment | comment?, threadId?, reply? | Opens a thread (comment with id, body, anchor, mentions) or replies to one (threadId + reply with id, body). Author and time are the event’s by and at; threads are numbered in order. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"comment.add","comment":{"id":"c0a8d1f2-7e44-4bd3-8f61-0c2b9e8d7a11","body":"Is R2 0.1 %?","anchor":{"kind":"item","itemId":"1f433314-e6de-4254-8c85-11b30802cc0d","at":[116,112]}}} |
comment.resolve | Resolve a comment thread | id, resolved? | Marks a thread resolved by by at at, or open again with resolved: false. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"comment.resolve","id":"c0a8d1f2-7e44-4bd3-8f61-0c2b9e8d7a11"} |
agent.session | Agent session | session | Marks the start, end or a note of an agent session (id, agent, model, summary, number of events) in meta.agentSessions; the session’s edits are the events between start and end with the same by. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"agent.session","session":{"id":"run-7","agent":"Claude","action":"start","summary":"Route the LED nets"}} |
tree.set | Set a value | path, value | Sets the value at path (JSON Pointer, @id segments) of the tree: the generic edit of the project, documentation, panel and requirements trees. /seq, /meta, /baselines, /comments and, when a KiCad source owns it, /document cannot be set. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"tree.set","path":"/requirements/requirements/@R-THERM-1/limit","value":85} |
tree.add | Insert into an array | path, value, before?, index? | Inserts value into the array at path: at position index when given, else before the element whose id is before, else at the end. A value with an id already in the array is skipped. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"tree.add","path":"/sections/variants/variants","value":{"id":"lite","name":"Lite","overrides":{}}} |
tree.remove | Remove a value | path | Removes the member or array element at path. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"tree.remove","path":"/settings/text_variables/REV"} |
document.add | Add a project document (prj only) | document | Adds a document (id, kind, path relative to the project, name, root) to the project. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"document.add","document":{"id":"6a0c1f1e-4c7e-4e0b-a3a0-0f7c5b0a9d01","kind":"pcb","path":"blinky.design.pcb","name":"Blinky"}} |
document.update | Change a project document (prj only) | id, set | Merges set into a document entry (path, name, root). | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"document.update","id":"6a0c1f1e-4c7e-4e0b-a3a0-0f7c5b0a9d01","set":{"path":"boards/blinky.design.pcb"}} |
document.remove | Remove a project document (prj only) | id | Removes a document entry and its sheet instances (the file is left alone). | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"document.remove","id":"6a0c1f1e-4c7e-4e0b-a3a0-0f7c5b0a9d01"} |
library.add | Add a library (prj only) | library, entry | Adds a library (name, uri, type, options, description) to the symbol or footprint table of the project. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"library.add","library":"footprint","entry":{"name":"blinky","uri":"${PROJECT}/blinky.design.fp"}} |
library.remove | Remove a library (prj only) | library, name | Removes a library from the symbol or footprint table. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"library.remove","library":"footprint","name":"blinky"} |
attachment.add | Reference a file | attachment | Adds an attachment: a file next to the design (relative path) identified by sha256 and size. The content never goes into the file. | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"attachment.add","attachment":{"id":"logo","path":"media/logo.png","sha256":"0000000000000000000000000000000000000000000000000000000000000000","size":1024,"role":"image"}} |
attachment.remove | Remove a file reference | id | Removes an attachment reference (the file is left alone). | {"seq":7,"at":"2026-10-18T09:00:00Z","by":"Ada","op":"attachment.remove","id":"logo"} |
6.4 Unknown operations#
An operation this build does not know is skipped with a warning and kept in the list: a file written by a later 1.x version opens, its unknown events stay, and the version that knows them applies them. Consolidation refuses to fold events that do not apply (§9).
6.5 Tree paths#
tree.set, tree.add and tree.remove name a place in the tree with a JSON Pointer (RFC 6901) in which a segment
@<id> names the element of an array whose id, uuid (the KiCad mapping’s objects) or name is <id>:
/requirements/requirements/@R-THERM-1/limit, /board/items/@5a0c…/start. tree.add inserts before the element
before names (an id or uuid), else at index, else at the end. Ids instead of positions keep the events of two branches
independent of each other’s inserts and deletes. /seq, /meta, /baselines and /comments are kept by their own
events and cannot be set.
7. Tolerance#
The reader is tolerant so that one broken line costs one line and not the file:
- the container is split into its members by a scanner and every list entry is parsed on its own; an entry that is not JSON (a truncated line, an object missing its closing brace) is dropped with a warning naming its line, and the reading goes on with the next line;
- lines of git conflict markers (
<<<<<<<,|||||||,=======,>>>>>>>) in the list are ignored: the events of both sides are kept, as a merge would (§15); - an entry without
opis dropped; a missing or repeatedseq(two branches appended the same number) is renumbered after the previous event; a missingattakes the previous event’s, a missingbybecomesUnknown; awal.basemarker anywhere but first is dropped; - in the tree, a missing member takes its default, a member of the wrong type is reset with a warning, entries of
comments,baselinesandattachmentswithout an id or reusing one are dropped; - unknown members anywhere (container, tree, objects, events) are kept and written back;
- events that do not apply are skipped and kept (§3).
Refused: an empty file, text that is not a JSON object, an unknown kind (when the file name does not tell either), a newer or unknown format version, and an older version that has not been upgraded (§14).
A file the reader had to repair is not a valid file: edsnx design validate reports the repairs as warnings and text
that is not strict JSON as an error, so the CI gate catches what a merge left behind.
8. Editing sessions and coalescing#
Dragging a footprint produces a move with every mouse step; typing a value produces an edit with every key. An
editing session merges a new event into the last unsaved event when both belong to one continuous edit: same by,
within 2 s, and either
- two updates of the same thing: moves of the same ids (vectors added), rotations of the same ids about the same
point (angles added),
setAttrs/setPad/setZone/symEditPin/symEditGraphic/fpEditPad/fpEditGraphic/fpEditText/document.updateof the same object (fields merged),setPropertyof the same field,symEditPropertyof the same key,meta.set(fields merged),tree.setof the same path,setBoardSetup(merged),setNetClasses/setDiffPairs/setMatchedGroups/setProjectSectionof the same section (replaced); or - a create followed by a move of exactly what it created: the create’s coordinates are shifted; it keeps its seed and ids, so the objects keep their uuids.
A line that is on disk is never changed: only unsaved events coalesce. Saving appends the unsaved events, one line each.
9. Consolidation#
Consolidation folds the list into the tree:
- the document is computed (§3); when an event does not apply, consolidation is refused unless the loss is
allowed explicitly (
--allow-loss), because folding would lose it; - every baseline created by a folded event gets a snapshot (§10);
- that document becomes the new tree, its
seqtheseqof the last event; - the list restarts with the marker (
seqthe newtree.seq,atthe time of the consolidation) and oneconsolidateevent (the nextseq) that records the range folded, kept inmeta.consolidations.
The design state is the same before and after; the individual events stay in the git history. Consolidation
rewrites the tree: consolidate on the main branch with no open branch holding unmerged events, and commit the result
alone. A list of only the last consolidate record has nothing to fold.
edsnx design consolidate blinky.design.pcb # fold
edsnx design consolidate . --dry-run --json # what would be folded, in every design file below .10. Baselines#
A baseline.create event names the state after itself: tree.baselines gets id, name, description,
createdAt and by (the event’s), seq (the event’s) and hash (the state hash, §3). While the events up to it are
in the list, the state is rebuilt by replaying them. When consolidation folds them, the baseline gets a snapshot: an
attachment (§13) holding the tree at that point as JSON, written to <file>.baselines/<baseline id>.json, so the
baseline can still be compared after the fold.
11. References between files#
A project lists its documents (documents, paths relative to the project file). A board refers to its requirements
and panels in sections, a schematic to its documentation sheets, every document to its project in meta.project,
all by meta.id with the path as a hint (§5). Library tables name libraries by URI; ${PROJECT} is the project
folder.
12. Canonical values#
- Numbers. Integers are written as they are. Other numbers are rounded to 15 significant digits, so float noise
such as
0.30000000000000004is written0.3and a value read back writes the same text;-0is written0. NaN and the infinities have no JSON form and are writtennull. Lengths are millimetres, angles degrees. - Keys. Sorted in code-unit order (
seqandmetafirst in the tree;seq,at,by,opfirst in an event). - Canonical JSON (hashes, ids derived from events, comparisons): keys sorted, numbers canonical, no whitespace,
undefinedmembers left out.
13. Attachments#
Images, 3D models, simulation results, Gerber sets and baseline snapshots are not embedded. An attachment is a
reference: id, path (relative to the design file’s folder, / separators, never absolute), sha256 (hex) and
size (bytes) of the content, and optional role (image, model3d, sim, gerber, snapshot, datasheet,
other), mime and name. attachment.add and attachment.remove change the list; the list never carries the
content. edsnx design validate checks that every attachment and snapshot is there with the recorded hash and size.
14. Versions and upgrades#
designVersion is a text of numbers separated by dots, compared number by number. This build reads and writes
1.0.
- A file of the current version is read.
- A newer version is refused with a message to update the Electronics Designer: a newer version may change the meaning of existing members. Within a version, additions (new operations, new members) are tolerated (§6.4, §7), so they do not need a new version.
- An older version is upgraded: a chain of steps, one per released version, each turning exactly one version into
the next (the tree and the events of the list alike) and appending a
format.upgradeevent (from,to,title) to the list, so the upgrade is a line of the history (meta.upgrades). Opening an older file upgrades it in memory;edsnx design upgraderewrites it on disk. - A version no release has written is refused.
Format 1.0 is the first version: its upgrade chain is empty.
15. Merging branches#
Both branches of a design file only appended events, so a merge keeps the common list and both tails:
- the two tails are interleaved: each keeps its own order; of the two next events, the one with the earlier
atgoes first, then the one whoseby, then whose content, sorts first. The result does not depend on which side is “ours”; - the merged events are renumbered after the common part; ids do not depend on
seq(§5), so no object changes; - an event both tails hold (a cherry-pick) is kept once;
- when one side consolidated, its tree already holds the common events: the other side’s tail goes after its list. Both sides consolidating differently cannot be merged; take one side and make the other side’s changes again.
What the merge guarantees. Independent edits (different objects, different fields of one object, new objects,
moves of one object on both sides, which add up) never conflict, and the merged document equals applying one tail
after the other, up to the order of the new objects in their arrays and the numbers derived from that order (net
codes, comment numbers). True conflicts are reported, not refused,
and resolved in merged order: the same field of the same object changed differently on both sides (the later
change wins), a move against a rotation or placement of the same object, an edit of an object the other side deleted
(an edit after the delete is skipped), and overlapping tree paths (/a set on one side, /a/b on the other).
edsnx design merge <base> <ours> <theirs> runs the merge and writes the result to <ours> (or --out); it is a git
merge driver:
git config merge.edsnx-design.name "SoftForge design files"
git config merge.edsnx-design.driver "edsnx design merge %O %A %B"
echo '*.design.* merge=edsnx-design' >> .gitattributesWithout the driver, git’s line merge of two appends leaves conflict markers around the two tails; the reader keeps
both sides (§7) and the next save writes a clean file, but edsnx design validate fails until then.
16. Command line#
edsnx design validate <file|folder>… [--strict] [--no-attachments] schema, reader, events, attachments; exit 1 on errors
edsnx design log <file> [--since 12|2026-10-18|2h] [--limit n] [--op move] [--by Ada]
edsnx design consolidate <file|folder>… [--allow-loss] [--no-snapshots] [--dry-run]
edsnx design upgrade <file|folder>… [--dry-run]
edsnx design merge <base> <ours> <theirs> [--out file] [--strict]
edsnx design import <kicad files|folders> --out <dir> [--report json] [--dry-run]
edsnx design export <design files|folders> --out <dir> [--report json] [--dry-run]
edsnx design roundtrip <kicad files|folders> [--strict] [--no-kicad-cli]
edsnx design migrate [project|folder] [--dry-run] [--report json]A folder stands for every .design.* file below it (node_modules, .git, out and *.baselines folders are
skipped). --json prints one JSON object; --actor sets the by of the events the commands write. Exit codes: 0
done; 1 an invalid file, a refused consolidation, a failed merge (or conflicts with --strict), an import or export
error, a round trip with a difference, a failed migration; 2 an unknown flag. import, export and roundtrip are
described in §18, migrate in §19.
edsnx design validate is the CI gate: errors are a file the reader refuses, a schema violation (text that is not
strict JSON included), an attachment that is missing or changed. Warnings are repairs the reader made, events that do
not apply and a layout that is not canonical; --strict makes them errors.
edsnx design validate . --json
{"ok":true,"errors":0,"warnings":0,"files":[{"path":"blinky.design.pcb","ok":true,"kind":"pcb","designVersion":"1.0","treeSeq":0,"events":18,"applied":18,"skipped":0,"canonical":true,"errors":0,"warnings":0,"issues":[]}]}17. JSON schemas#
schemas/design/ (shipped with the extension) holds draft 2020-12 schemas under
https://soft-forge.dev/schemas/design-1.0/:
| Schema | Describes |
|---|---|
design.schema.json | the container of any kind (the tree is checked by kind) |
prj.schema.json … req.schema.json | the container of one kind (named by $schema in every file) |
common.schema.json | shared definitions: meta, attachment, baseline, comment thread, the trees, the renderer document |
events.schema.json | the list: the marker, the envelope and the payload of every edit intent and file operation |
The schemas describe what the Electronics Designer writes, and accept what the reader tolerates on purpose: unknown members and
unknown operations. They are generated from src/design/schemas.ts (npx tsx scripts/gen-design-schemas.ts).
18. KiCad mapping#
The KiCad importer (src/design/kicad/importer.ts, edsnx design import) reads KiCad 6–10 files into design files;
the exporter (src/design/kicad/exporter.ts, edsnx design export) writes them back. A file imported and not edited
comes back byte for byte when KiCad 8 or later wrote it (KiCad’s own layout: tabs, xy runs, lists wrapped at
column 72); files of KiCad 6 and 7 and files written by hand come back with the same tokens, which kicad-cli … upgrade --force re-saves to the same file. An edited file is printed in KiCad’s layout.
18.1 Files#
| KiCad file | Design file | Tree path |
|---|---|---|
.kicad_pcb | .design.pcb | /board (the kicad_pcb list), /x-kicad, /document (reduced) |
.kicad_sch (every sheet) | .design.sch | /schematic, /x-kicad, /document |
.kicad_sym | .design.sym | /library, /x-kicad, /document |
.kicad_mod | .design.fp (<lib>.pretty/ kept as a folder) | /footprint, /x-kicad, /document |
.kicad_pro | .design.prj (one per project) | /settings (every member but softforge), /sections (softforge), the views /rules/netClasses, /rules/diffPairs, /rules/matchedGroups |
.kicad_prl | .design.prj | /localSettings |
fp-lib-table, sym-lib-table | .design.prj | /libraries/footprint, /libraries/symbol (name, type, uri, options, description, disabled, hidden) |
.kicad_dru | .design.prj | /rules/custom (the text, comments included), /rules/customRules (the rules mapped) |
.kicad_wks (named by the project, inside its folder) | .design.prj | /worksheets/<n> (path, sheet mapped) |
| sheet files of a schematic | .design.prj | /documents (root sheet and board marked root), /hierarchy (sheet instances: docId, parent, name, page) |
(image (data …)), (embedded_files (file (data …))) | attachment | <design file>.files/<sha256 prefix>[-name].<ext>; the node keeps {"$attachment": id, "form", "chunks"} |
(model "…") of a footprint, Sim.Library fields | attachment by reference | when the file is inside the project folder; ${KICAD…} library paths stay paths (reported) |
Every design file gets an import.run event (importer kicad, the sources with their sha256) and meta.project
points at the project. JSON spellings that canonical JSON would change (0.0, 1e-05) and key orders that are not
sorted are kept per JSON pointer in x-kicad.json of the project tree; the file names and the library tables’ layout
(KiCad 6–9 one-line rows or KiCad 10) in x-kicad.files and x-kicad.libTables. Text variables, title blocks,
groups, tuning patterns (generated), embedded fonts and every other node are part of the mapped trees.
18.2 Nodes#
A KiCad list (head atom… (child …)… flag…) maps to a JSON object:
| KiCad | JSON |
|---|---|
| leading atoms | named members where KiCad’s grammar names them ((footprint "Lib:R" …) → "name": "Lib:R", (pad "1" smd rect …) → "number", "type", "shape"), else _ |
| a child of atoms only that occurs once | its atom or an array: "width": 0.25, "at": [10, 20, 90] |
| a child that occurs once | a member named after its head: "layer": "F.Cu", "stroke": {…} |
repeatable children (footprint, segment, pad, fp_line, property, pin, wire, xy, lib, rule, …) | items, in file order: objects with $type, or [head, …atoms] for a list of atoms only (["net", 0, ""], ["xy", 1, 2]) |
bare atoms after the children (KiCad 6–7 hide, locked) | flags |
| numbers | JSON numbers when the canonical spelling is KiCad’s (0.25, -2); otherwise strings ("1.0") |
| an atom quoted (or bare) against the rule of its place | {"$str": …}, {"$sym": …}; a token that is neither, {"$raw": …} |
| layout other than KiCad 8+’s own | $flat (on one line), $lines (one xy per line), $tight (closing after the last child), $order (member order) |
| a node outside KiCad 6–10’s vocabulary | opaque item {"x-kicad": "(…)"} (with $flat when it was on one line), counted in the loss report |
| text before or after the root list | x-kicad.lead, x-kicad.tail of the tree, counted in the loss report when it is not whitespace |
The vocabulary, the repeatable children, the member orders and the quoting rules are tables generated from KiCad’s
keyword lists and the demo corpus (scripts/gen-kicad-tables.ts → src/design/kicad/tables.ts).
18.3 What stays KiCad text#
Only what KiCad itself does not define: nodes outside the vocabulary of KiCad 6–10 (a newer KiCad’s node, another
tool’s extension) as opaque items, and text after the root list (a file another tool appended to). Both come back as
they were. The .kicad_dru text is kept with its comments next to the mapped rules: the exporter writes the text
unless the mapped rules were changed. Files the importer does not read: .kicad_jobset, fp-info-cache, backups and
lock files, the Electronics Designer’s sidecar files (.kicad_doc.json and the like).
18.4 Loss report and commands#
The importer and the exporter return a loss report: one entry per file, tree path and node with the reason and a
count, of kind opaque (kept as text), dropped (no place in the target: a documentation, panel or requirements file
has no KiCad file), reference (a 3D model, simulation library or worksheet left as a path) and layout (a file not
in KiCad’s own layout: it comes back with the same tokens, not the same bytes). Errors (a graphic tree drawn
natively, with no mapping yet; a missing attachment; an event that does not apply) fail the command.
edsnx design import blinky/ --out design/ --report json
{"ok":true,"dryRun":false,"designs":[{"path":"design/blinky.design.pcb","kind":"pcb","sources":["blinky/blinky.kicad_pcb"]},…],"attachments":[…],"report":{"files":8,"designs":7,"opaque":0,"dropped":0,"references":5,"layout":0,"entries":[…],"errors":[]}}
edsnx design export design/ --out kicad/
edsnx design roundtrip blinky/ --json
{"ok":true,"files":8,"identical":8,"resaveIdentical":0,"whitespace":0,"different":0,"kicadCli":"10.0.7","withLoss":0,…}edsnx design roundtrip imports, writes and reads the design files, exports and compares: identical bytes pass; with
kicad-cli a file whose bytes differ passes when both re-saves are the same (resaveIdentical), without it when only
whitespace differs; --strict asks for identical bytes. It exits 1 on any other difference, so it gates CI. The
round-trip gate of the test suite (test/design.kicad.roundtrip.test.ts, KICAD_SLOW=1) runs it over the test
fixtures, the examples and the KiCad demos (KICAD_DEMOS), and for every file that is not byte-identical also
compares the netlist and ERC (schematics), the DRC report and the Gerber and drill plots (boards) of the original
and the exported project.
19. Editors, compatibility mode and migration#
The board, schematic, symbol, footprint and panel editors, the headless session (edsnx serve, MCP, the CLI) and the
agent session work on design files (src/design/host.ts, src/design/backing.ts). A document is the tree with the
list applied up to a cursor:
- an edit intent runs the editor’s reducer with uuids drawn from the event it is recorded as (§5), so the replay
creates the same objects; the event is recorded with the ids it created (
ids); - an intent whose result depends on a service of the editor that a reader may not have (
fillZonesandhealZoneswith the zone filler,applyNetlist, abatchholding one) is recorded as the tree operations of its result instead, so every reader gets the same document; - an edit that is not an intent (a text rewrite, an external change, a hot-exit restore) is recorded as tree
operations:
tree.set/tree.add/tree.removeelement by element (byid/uuid, by position otherwise), checked by applying them, a wholetree.setof the mapping when the diff does not reproduce the text; - project settings changed by an editor (design rules, net classes, exclusions, text variables) are tree operations of
the
.design.prjunder/settings,/sectionsand/rules; - undo and redo move the cursor; a new edit drops the events after it. Save appends the events after the last save;
when the cursor went below it, the file is written whole with the list up to the cursor. A baseline
(
baseline.create) is one more event and one undo step; - consolidation (§9) requires a saved document, checks that the folded tree gives the editor’s document, and clears
the undo history. It runs on save once the list is longer than
softforge.edsnx.design.consolidateAfterevents.
The editors’ text of a graphic file is the KiCad text of its mapping (§18); every view, check, output and command
downstream reads that text and the renderer document as before. A graphic tree drawn natively (no mapping) is edited
on its document; its KiCad export is not available yet.
19.1 meta.kicad#
| Value | The design file is | Written |
|---|---|---|
| absent | a design file of its own | by the editors |
"cache" | the compatibility cache of the KiCad file next to it | after the KiCad file, on its save |
"export" | authoritative; the KiCad file next to it is an export target | by the editors, then the KiCad file is exported |
Compatibility mode. A KiCad file of a KiCad project (a folder with a .kicad_pro and no migrated .design.prj)
stays authoritative, with name.design.<kind> next to it (and name.design.prj for the project):
- open: the KiCad file is imported (in memory, nothing written); a cache whose tree equals the import is used as it
is; a cache that differs gets the differences as tree operations followed by an
import.runwithnote: "resync", so its history is kept; without a cache the import is the start of its list. The loss report goes to the Problems view and the editor’s banner; - edit: intents and rewrites are recorded in the cache as above; the KiCad text is the editor’s document;
- save: the KiCad file is written as before, then the cache (and the
.design.prjcache for the project settings). Undone edits leave the KiCad file byte-identical.
softforge.edsnx.design.compatibilityCache: false turns the cache off.
Migration. edsnx design migrate <project> (and Electronics Designer: Migrate Project to Design Files,
softforge.edsnx.design.migrateProject, registry project.migrateDesign) imports every KiCad file of the project
folder next to it with meta.kicad: "export"; a cache in step keeps its list and gets a meta.set. The KiCad files are
not changed. From then on the design files are opened (the Project view’s nodes, edsnx serve), and every save also
exports the KiCad file and the .kicad_pro, so kicad-cli, KiCad and the fabrication outputs keep working.
edsnx design migrate blinky/ --dry-run --json
{"ok":true,"dryRun":true,"project":"blinky/blinky.kicad_pro","designs":[{"path":"blinky/blinky.design.prj","kind":"prj","kept":false,"sources":["blinky/blinky.kicad_pro","blinky/blinky.kicad_prl"]},…],…}20. Importers and exporters#
Design files are the Electronics Designer’s own format; other formats come in through importers and go out through
exporters registered in one registry (src/design/interop.ts). edsnx import / edsnx export, the registry commands
project.import / project.export / project.formats, the Project view’s Import… / Export…, the
open-foreign-file flow and add-ons (api.interop) all use it.
edsnx import <files|folders> --to <dir> [--importer id] [--options json] [--report json] [--dry-run]
edsnx export <design files|folders> --as <exporter> --out <dir> [--options json] [--report json] [--dry-run]
edsnx project formats --jsonedsnx import sends each file to the first importer that reads it (by extension, then by content when formats share an
extension), a folder to the first importer, in registry order, that reads something in it (design formats before fab
sets); --importer picks one. edsnx export takes design files (a folder: every design file below it); KiCad files, or
a KiCad project folder without design files, are imported in memory first. Exit codes: 0 done, 1 an error in the
report or an unknown importer / exporter, 2 an unknown flag. JSON output (--json, --report json):
edsnx import divider.brd divider.sch --to design/ --report json
{"ok":true,"dryRun":false,"importers":["eagle"],"designs":[{"path":"design/divider.design.prj","kind":"prj","sources":[…]},…],"attachments":[],"report":{…}}
edsnx export design/ --as gerber --out fab/ --json
{"ok":true,"dryRun":false,"exporter":"gerber","designs":[…],"files":[{"path":"fab/divider-F_Cu.gtl","bytes":1834,"design":"design/divider.design.pcb"},…],"report":{…}}20.1 The contract#
An importer (ImporterDef) has an id (add-ons: <publisher>.<name>), a name, the extensions it reads (with
the dot; an entry without a dot is a whole file name, fp-lib-table), an optional accepts(file, head) content check
(head: the first 4 KiB) and import(files, ctx). The context gives the output folder (outDir), the inputs’ common
folder (root: a design lands at its source’s place relative to it), by / at / generator for the new trees, a
read(path) and a list(dir) callback and the options. It returns the design files (absolute path, the
DesignFile, the sources), attachment files to write next to them and the loss report. Each design’s list is
wal.base followed by an import.run event naming the importer, its version and the sources with their sha256 (§6.3).
An exporter (ExporterDef) has an id, a name, the design kinds it writes, the extensions of its files,
needsKicadCli, a sentence on what the format loses, and export(designs, ctx). Each design comes with its file
and its document (tree: the list applied, §3). The context adds kicad(), the designs written as KiCad files in a
scratch folder (fileFor(designPath)), for exporters built on KiCad tooling, kicadCli(), scratch() and
readAttachment(). It returns the files (absolute paths under outDir) and its report; designs of other kinds are
reported as dropped.
A second registration of an id replaces the first until it is disposed. Importers of other formats may write KiCad text
and hand it to the KiCad mapping (src/design/interop/viaKicad.ts): their designs are then ordinary mapped trees
(§18), edited through the KiCad reducers and exported to KiCad as they are.
20.2 Loss report#
The report of §18.4, for every importer and exporter: files read or written, designs, counts per kind and
entries (file relative to the inputs’ folder, path, node, kind, reason, count) and errors. Kinds:
dropped (not carried over), approximated (carried over, not exactly: a shape, a font, a rule the target only
approximates), opaque (kept as text that is not mapped), reference (a file referred to and not attached) and
layout (the same content in another layout). An error fails the command; losses do not. The VS Code side lists the
last report in the Problems view (KiCad: Show Import Report) and the editor of an imported design shows its counts.
20.3 Built-in importers#
| Id | Reads | Writes | Reported |
|---|---|---|---|
kicad | KiCad 6–10 files (§18) | every kind but doc, panel, req | §18.4 |
eagle | Eagle 6+ XML .sch, .brd, .lbr (<eagle in the first 4 KiB) | .design.sch, .design.pcb, .design.sym + .design.fp per package; a schematic and a board of one name share a .design.prj | design rules, autorouter and CAM settings, attributes and layers with no KiCad place, vector fonts, busses and frames (see the user guide) |
gerber | a fab set: Gerber X1 / X2 layers, Excellon drill files, the .gbrjob (a folder, a zip, or one file: its folder’s set) | <set>.design.pcb + .design.prj | nets, footprints, components and zones (not inferable), drill maps and drawings, clear-polarity objects; plated holes without a round pad, slots and polygons with holes as approximations |
The Gerber importer reads the set with the native Gerber engine (src/gerber). Layers go to the KiCad layer of their
file function (X2 .FileFunction, the job file, else the file name): draws with round apertures become gr_line /
gr_arc of the aperture’s width, round flashes filled gr_circle, rectangular flashes filled gr_rect, regions and
every other aperture filled gr_poly (curves as chords of the engine’s tolerance), the profile lines on Edge.Cuts.
Plated holes become vias: the round flash both outer copper layers have at the hole becomes the via’s pad. Slots and
non-plated holes become footprints of one pad with hidden texts. The re-export of the sample project’s set with the
native engine compares equal per layer with the parity tooling (XOR below 0.01 %, the same holes).
20.4 Built-in exporters#
| Id | Writes | kicad-cli | Loses |
|---|---|---|---|
kicad | the KiCad files of every design (§18) | no | doc, panel, req; graphic trees drawn natively |
gerber | per board: Gerber X2 layers, Excellon drill files, the job file (native engine; options: the Plot dialog’s) | no | everything but the layer images (nets as X2 attributes) |
odb | per board: <board>-odb.zip | 9+ | the schematic, rules, Electronics Designer sections |
ipc2581 | per board: <board>-ipc2581.xml | 8+ | the schematic, rules, Electronics Designer sections |
dsn | per board: <board>.dsn (Specctra, the autorouter exporter) | no | silkscreen, fab and mask graphics, text, zone fills |
idx | per board: <board>.idx (IDX 3.0 baseline, the MCAD exporter; the board’s mcad section) | no | copper, nets, text |
pdf | per board: one PDF per layer; per root schematic: every sheet in one PDF | yes | everything but the drawing |
svg | per board: one SVG per layer; per root schematic: one SVG per sheet | yes | everything but the drawing |
json | per graphic design: <name>.<kind>.json, the reduced document (option pretty) | no | the mapping, sections and list; not read back |