The Definition System
This chapter explains the model behind definition files: the element tree, type paths, entity identity, workspace discovery, and how definitions and configurations are validated together.
File Kinds
| Suffix | Meaning |
|---|---|
|
Definition file, describes the configuration schema model.
|
|
Configuration file, holds configuration data and binds to one definition. |
|
Generated JSON Schemas.
Artifacts of |
|
Workspace settings file, see Workspace Discovery. |
The suffix is the source of truth for the file type. Confµgami never infers the kind of a file from its directory or content.
The Element Tree
A definition describes a tree of elements. Each element is declared under a property name (also called its relation name), the key the configuration author later writes. Three families of elements exist:
- Aggregates
-
-
An entity groups children and has identity: in the configuration it can carry a
nameorid, can be the target of references, and is the unit of cross-file merging. -
A struct groups children without identity. It cannot be referenced or merged and is meant for grouping parameters.
-
A union is a choice between several entity or struct types.
-
- Parameters
-
Scalar values:
bool,string, the integer familyu8,i8,u16,i16,u32,i32,u64,i64, the floatf64,enum,linkerSymbol, andfilePath. - References
-
A
refelement points at an entity elsewhere in the configuration. References are how definitions model wiring and resource allocation, for example "the sensor supervised by the watchdog".
Every element can carry title, description (Markdown), $comment (ignored by tooling, for readers of the definition), a multiplicity, and custom annotations.
See the type reference for all per-type options.
Multiplicity
multiplicity: "min..max" states how often the element may appear (optional, required or list).
Both min and max values can be changed.
You can use * in the upper multiplicity to say unlimited (unbounded).
When the key is omitted the default is "0..1": optional, at most one.
A trailing ? makes a list optional as a whole, but a configured list has to satisfy the lower bound.
3..4? therefore accepts three or four elements, or nothing at all, while 3..4 requires three or four in every configuration.
| Multiplicity | Meaning |
|---|---|
|
Optional single value (the default) |
|
Required single value |
|
Optional list, any count |
|
Required list with at least one element |
|
Required list with two to ten elements |
|
Required list with exactly 16 elements |
|
Optional list with two to ten elements |
|
Optional list with exactly 16 elements |
Anything with an upper bound above one is a list, and an entity or struct list then needs a plural name (see the naming rules in the reference).
There is no separate required flag: whether an element is required follows from the lower bound of its multiplicity, except there is an ? for optional lists.
Use 1..1 only for truly required data, values without which the element is meaningless.
For everything else prefer an optional field (0..1) with a well-chosen default: the configuration stays small, and the fallback behavior is documented in the definition instead
of in the consuming code.
The same applies to lists: choose 1.. over 0.. only when an empty list is genuinely invalid, and say so in the description.
No Null
JSON allows null as a value, but Confµgami has no null type: null is rejected in configurations and never appears in generated schemas or bindings.
Absence is expressed structurally instead.
Model the field as optional (0..1), and the configuration author omits it entirely when there is no value.
Type Paths
Every element has a type name and, through its position in the tree, a type path.
Type paths are used wherever the definition refers to a type: inherits, typeRef, union typeRefs, and refTargetTypes.
-
The definition itself is the root type:
TurnIndicator. -
Each element contributes its type name: the
indicatorproperty of theTurnIndicatordefinition has the type pathTurnIndicator::Indicator. -
For a single element the type name is derived from the property name by uppercasing the first letter (
indicatorbecomesIndicator). A list property is not de-pluralized automatically: it must set an explicittypeNamegiving the singular of the plural property name (sensorsrequirestypeName: "Sensor"). SettingtypeNamealso overrides the derivation for single elements.
A nested element therefore has a path like SensorEcu::Ecu::Sensor: the definition name, then each ancestor’s type name, joined with ::.
Entity Identity
In the configuration, every entity implicitly supports two properties:
-
name: a human readable identifier, used for referencing and error reporting. -
id: a globally unique identifier, used for global referencing. The id also serves as the name.
An entity instance carries name or id, never both.
An entity’s path segment in reference paths is its name, else its id, else its type name. Identity matters in three places: reference targets, error messages, and cross-file merging (entities with the same identity merge, see Multi-file configurations).
Deriving the Name or Id from a Parameter
Often the real, user facing identity of an entity is already one of its parameters, and a separate name would only repeat it.
nameSource names the parameter that supplies the name instead.
{
$definitionSpecVersion: "1.0",
definitionName: "Bus",
definitionVersion: "1.0",
configurationFileMode: "SingleFile",
definition: {
namespaces: {
typeName: "Namespace",
multiplicity: "0..*",
nameSource: "fqn",
title: "Namespace",
entity: {
fqn: {
multiplicity: "1..1",
title: "Fully qualified name",
description: "The real, user facing identity of the namespace.",
string: {
pattern: "^[A-Za-z0-9_:]+$",
minLength: 1,
},
},
signals: {
typeName: "Signal",
multiplicity: "0..*",
idSource: "symbol",
entity: {
symbol: {
multiplicity: "1..1",
title: "Symbol",
description: "The symbol naming this signal in the generated code.",
string: {
minLength: 1,
},
},
},
},
},
},
},
}
A configuration then writes only that value:
{
$definition: "Bus@1.0",
namespaces: [
{
// No 'name': the entity is called 'com__vector__Engine', mangled from this value.
fqn: "com::vector::Engine",
signals: [
{
symbol: "engine_rpm"
},
],
},
],
}
idSource works the same way and produces an id, which is globally unique and also sets the name.
The two are mutually exclusive.
The rules:
-
The named property is an own or inherited
1..1string parameter of that entity, and it must not declare adefault. -
A written
nameoridwins, and the two may disagree without a diagnostic. -
Otherwise the name is the source value with every character outside
[A-Za-z0-9_]replaced by. If that does not start with a letter, the entity’stypeNameand aare prefixed, so"::Your Value"undertypeName: "Namespace"becomesNamespace___Your_Value. -
Only a list multiplicity may declare a source. At
0..1or1..1the definition name is already stable and unique, so a source would only make every reference path depend on a freely edited value. -
The root definition entity must not define a source, since its name is what
$definitionand every absolute path resolve against. -
A subtype inherits the source of the entity it
inheritsfrom and may override it with its own.
The value has to be a literal in the entity’s own object.
The name is built during the parse, before mixins are applied and before .default / .preconfig values are merged, so a value arriving from any of those cannot name the entity.
The merged output carries the synthesized name (and id), so a consumer never re-derives it.
The generated JSON Schema carries a nameSource / idSource keyword on the entity type saying which property supplies it.
Workspace Discovery
cfu (CLI, LSP, and MCP alike) operates on a workspace: the set of definition and configuration files it discovers from its start directory.
- Finding the workspace root
-
From the start path, Confµgami searches upward for the first of: a
.confufile withworkspaceRoot: true, or a repository marker (.git,.svn,.vscode,platformio.ini). The search stops at filesystem boundaries such as the home directory. - Collecting files
-
From the root, all
.def.json5and.cfg.json5files (and their.jsonvariants) are collected recursively. Hidden directories (starting with.),node_modules, and directories marked with aCACHEDIR.TAGor.cargo-lockfile, or with a.confusettingignoreTraversal: true, are skipped. - The
.confufile -
A JSON5 settings file that shapes discovery:
{ workspaceRoot: true, // this folder is a workspace root cfgSpace: false, // this folder is its own CfgSpace (see below) ignoreTraversal: false, // do not descend below this folder definitions: ["../defs"], // additional folders to load definitions from }workspaceRootandcfgSpaceare mutually exclusive, as arecfgSpaceandignoreTraversal.workspaceRootandignoreTraversalmay be combined: the folder is hidden from enclosing workspaces but scanned as its own workspace when passed directly as the input path. Thedefinitionspaths are resolved relative to the.confufile and let a project pull in definitions that live outside its tree, for example a shared platform definition folder. - CfgSpaces
-
Configuration files are grouped into CfgSpaces (configuration spaces, projects). A folder with
cfgSpace: truein its.confuforms an explicit space containing all configuration files below it; each workspace root forms a synthetic space for the remaining files. Validation, and in particular cross-file merging, happens per CfgSpace: two files merge only when they are in the same space. This is how one workspace can hold several independent projects configured from the same definitions.
How a Configuration Binds to a Definition
A configuration file declares $definition: "Name@version".
The name is resolved against all definitions discovered in the workspace; if it is unknown, the diagnostic lists the available names.
Definition names must therefore be unique per workspace.
The version after the @ is required, records which version of the definition the file was written for and is checked against definitionVersion in the definition (see Versioning).
The long form gives it as a separate $definitionVersion key instead, and using both at once is an error.
Generated JSON Schemas are an additional, editor-oriented layer: they give schema-aware editors completion without the LSP. The authoritative validation is always the definition itself.
The Validation Flow
Validation runs in phases, and later phases run even when earlier ones report problems, so you always get partial results and as many diagnostics as possible in one pass:
-
Every file is parsed (JSON5 syntax, structure).
-
All definitions are parsed into the definition model and checked (naming rules, type resolution, inheritance cycles, ranges).
-
Each configuration is resolved against its definition: unknown properties, type mismatches, value ranges, multiplicities, references.
-
Cross-file checks per CfgSpace: file mode violations, duplicate identities.
-
In mergeable modes, entities are merged and the merged result is re-checked (merge conflicts, multiplicities "across merged files").
-
The definition’s constraints run over the configuration (see Constraints Across Several Values).
Diagnostics carry file, range, severity, and often a related location (for example both sides of a merge conflict).
The same engine drives the CLI (cfu def validate, cfu cfg validate), the LSP server in the IDE, and the MCP server for AI tooling; a definition author only has to learn one set
of messages.
Constraints Across Several Values
min, max, pattern and the multiplicities check each value on its own.
A check validating values, such as "an FD controller needs a data bitrate" or "the tasks of a core must not overload it", is a constraint written in the definition:
canControllers: {
typeName: "CanController",
multiplicity: "0..*",
constraints: [
{
name: "FdNeedsDataBitrate",
assert: "mode != 'FD' || $exists(dataBitrate)",
message: "FD controller '{name}' needs a data bitrate.",
},
],
The editor and cfu cfg validate report a violation like any other diagnostic.
See Constraints for details.
Versioning
$definitionSpecVersion-
The version of the definition language itself. Checked against what the Confµgami executable supports. Currently
"1.0". definitionVersion-
The version of your model,
major[.minor[.patch]]. Bump it when you change the definition; the configuration side records the version it was written against in its$definition. That configuration stays valid for every definition version of the same major and a same or higher minor and patch, for0.xa minor change breaks.