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 number tree.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.

json
{
  "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.

ExtensionKindTree holds
.design.prjprjthe project: documents, sheet hierarchy, library tables, net classes and rules, settings, project sections, reviews
.design.schschone schematic sheet (renderer document, sections)
.design.pcbpcbone board (renderer document, board sections: flex, SI, MCAD, rooms, test points, references to requirements and panels)
.design.symsyma symbol library
.design.fpfpone footprint
.design.docdoca documentation sheet (views, tables, balloons, dimensions, notes)
.design.panelpanela panel (boards, layout, rails, separation)
.design.reqreqthe requirements of a board (families, requirements, results, history)

Members of the container, in the order they are written:

MemberTypeMeaning
designVersionstringFormat version, numbers separated by dots (§14). Required.
kindstringOne of the kinds above. Required; a reader falls back to the file name when it is missing.
$schemastringThe JSON schema of the kind, written on every save, ignored by the reader.
$commentstringA fixed note for people reading the raw file, written on every save, ignored by the reader.
otheranyUnknown members (for example x-… extensions of other tools) are kept and written back, sorted.
treeobjectThe consolidated document (§4).
walarrayThe 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, \n line ends, a final line end; two-space indentation;
  • the container members in the order above; in tree, seq and meta first, 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]];
  • wal with 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:

MemberTypeMeaning
seqinteger ≥ 0The tree is the document after this event.
metaobjectid (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.
commentsarrayComment threads (id, number, author, at, body, anchor, replies, state).
baselinesarrayNamed states (§10).
attachmentsarrayFiles the design refers to (§13).

The members of each kind:

  • Graphic kinds (sch, pcb, sym, fp): document, the renderer model (kind schematic / pcb / symbol / footprint, title, bounds, layers, primitives, items, and for boards nets, setup, stackup); sections, the parts of the design that are not drawn (board: flex, si, mcad, idx, rooms, testPoints, requirements, panels; schematic: docs). Items carry id, kind, label, at and their attributes; primitives carry type, layer and the itemId of the item they draw.
  • Project (prj): documents (id, kind, path relative to the project, name, root), hierarchy (sheet instances: id, docId, parent, name, page), libraries (symbol and footprint tables: 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#

MemberTypeMeaning
seqintegerStrictly increasing; the first event follows tree.seq.
atstringISO 8601 time with a zone (2026-10-18T09:00:05.000Z).
bystringAuthor: a user name, an agent name or a tool.
opstringThe operation: camelCase for an edit intent, dotted for a file operation.
payloadThe 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:

json
{"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#

OperationWhatPayloadEffectExample line
wal.baseList 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.setSet document metadatasetMerges 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.runRecord an importimporter, 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}
consolidateRecord a consolidationfrom, to, eventsWritten 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.upgradeRecord a format upgradefrom, 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.createCreate a baselinebaselineNames 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.deleteDelete a baselineidRemoves 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.addCommentcomment?, 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.resolveResolve a comment threadid, 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.sessionAgent sessionsessionMarks 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.setSet a valuepath, valueSets 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.addInsert into an arraypath, 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.removeRemove a valuepathRemoves 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.addAdd a project document (prj only)documentAdds 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.updateChange a project document (prj only)id, setMerges 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.removeRemove a project document (prj only)idRemoves 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.addAdd a library (prj only)library, entryAdds 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.removeRemove a library (prj only)library, nameRemoves 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.addReference a fileattachmentAdds 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.removeRemove a file referenceidRemoves 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 op is dropped; a missing or repeated seq (two branches appended the same number) is renumbered after the previous event; a missing at takes the previous event’s, a missing by becomes Unknown; a wal.base marker 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, baselines and attachments without 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

  1. 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.update of the same object (fields merged), setProperty of the same field, symEditProperty of the same key, meta.set (fields merged), tree.set of the same path, setBoardSetup (merged), setNetClasses / setDiffPairs / setMatchedGroups / setProjectSection of the same section (replaced); or
  2. 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:

  1. 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;
  2. every baseline created by a folded event gets a snapshot (§10);
  3. that document becomes the new tree, its seq the seq of the last event;
  4. the list restarts with the marker (seq the new tree.seq, at the time of the consolidation) and one consolidate event (the next seq) that records the range folded, kept in meta.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.

text
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.30000000000000004 is written 0.3 and a value read back writes the same text; -0 is written 0. NaN and the infinities have no JSON form and are written null. Lengths are millimetres, angles degrees.
  • Keys. Sorted in code-unit order (seq and meta first in the tree; seq, at, by, op first in an event).
  • Canonical JSON (hashes, ids derived from events, comparisons): keys sorted, numbers canonical, no whitespace, undefined members 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.upgrade event (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 upgrade rewrites 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 at goes first, then the one whose by, 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:

text
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' >> .gitattributes

Without 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#

text
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.

text
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/:

SchemaDescribes
design.schema.jsonthe container of any kind (the tree is checked by kind)
prj.schema.json … req.schema.jsonthe container of one kind (named by $schema in every file)
common.schema.jsonshared definitions: meta, attachment, baseline, comment thread, the trees, the renderer document
events.schema.jsonthe 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 fileDesign fileTree 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 fieldsattachment by referencewhen 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:

KiCadJSON
leading atomsnamed 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 onceits atom or an array: "width": 0.25, "at": [10, 20, 90]
a child that occurs oncea 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
numbersJSON 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 vocabularyopaque item {"x-kicad": "(…)"} (with $flat when it was on one line), counted in the loss report
text before or after the root listx-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.

text
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 (fillZones and healZones with the zone filler, applyNetlist, a batch holding 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.remove element by element (by id / uuid, by position otherwise), checked by applying them, a whole tree.set of 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.prj under /settings, /sections and /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.consolidateAfter events.

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#

ValueThe design file isWritten
absenta design file of its ownby the editors
"cache"the compatibility cache of the KiCad file next to itafter the KiCad file, on its save
"export"authoritative; the KiCad file next to it is an export targetby 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.run with note: "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.prj cache 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.

text
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.

text
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 --json

edsnx 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):

text
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#

IdReadsWritesReported
kicadKiCad 6–10 files (§18)every kind but doc, panel, req§18.4
eagleEagle 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.prjdesign rules, autorouter and CAM settings, attributes and layers with no KiCad place, vector fonts, busses and frames (see the user guide)
gerbera 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.prjnets, 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#

IdWriteskicad-cliLoses
kicadthe KiCad files of every design (§18)nodoc, panel, req; graphic trees drawn natively
gerberper board: Gerber X2 layers, Excellon drill files, the job file (native engine; options: the Plot dialog’s)noeverything but the layer images (nets as X2 attributes)
odbper board: <board>-odb.zip9+the schematic, rules, Electronics Designer sections
ipc2581per board: <board>-ipc2581.xml8+the schematic, rules, Electronics Designer sections
dsnper board: <board>.dsn (Specctra, the autorouter exporter)nosilkscreen, fab and mask graphics, text, zone fills
idxper board: <board>.idx (IDX 3.0 baseline, the MCAD exporter; the board’s mcad section)nocopper, nets, text
pdfper board: one PDF per layer; per root schematic: every sheet in one PDFyeseverything but the drawing
svgper board: one SVG per layer; per root schematic: one SVG per sheetyeseverything but the drawing
jsonper graphic design: <name>.<kind>.json, the reduced document (option pretty)nothe mapping, sections and list; not read back

Documentation for Electronics Designer 2.3.60.