OpenEik.org

Draft OpenEik 0.1.0-draft: draft, not final. From commit c6463c72 of 30 October 2026. Errata: the errata of 0.1. Raw text: /spec/0.1/commands.md

On this page

OpenEik 0.1 specification

Part V. Commands

A command is a typed, validated change to a model. Commands are how a host changes a model on behalf of a person or an agent: each is checked before anything changes, applied all or nothing, and answered with what it touched. Hand edits of files are also allowed (Part III, section 14); they are checked when the folder is read.

1. The command

  1. A command is a JSON object whose kind names it; its other members are fixed by its kind.
  2. A command processor MUST refuse a command whose kind it does not implement, and a command that does not match its kind's schema, naming the member that is wrong.
  3. The records a command carries (an element in create, a type in type.upsert, a view in view.upsert) and an element's patch MUST match their schemas. A member the schemas do not know MUST be kept in the record it is written to (Part I, section 10); the processor SHOULD warn, naming it, since it is often a misspelling. An extension member (Part I, 11.2) is no misspelling, and needs no warning. But a processor MUST refuse a command that would write an extension member whose value is null into a record that did not hold it, naming the record and the member (Part I, 11.2, rule 3): in a record a command writes whole (create and the upserts) and in the value a patch gives a member, null is a value, and only a member of the patch itself takes a member away with null (4.2). One that the record held, and that the command carries back as it was read, is kept (Part I, 10, rule 1).
  4. A member a command's kind does not define at the top level of the command (for example a misspelt expectedRevison) MUST NOT be dropped silently: the processor SHOULD refuse the command or warn.
  5. A processor MUST NOT create an element or a type of a category it does not know, and MUST keep those a model holds, through every command that does not delete them (Part I, 10, rule 5). A processor asked for a proposal for 0.2 that adds categories knows them: Part XIII's electrical, cableCarrier and cable, Part II, 6.2.1's member, and Part II, 6.10's provision. It makes and changes records of them by that proposal's rules: it holds each to the proposal's schema, refuses what the proposal's rules refuse where it refuses the like of a category of 0.1 (a host of another category, a level that is not there, a size the type sets: section 6, stage 2), and checks the whole model with the proposals it is asked for (stage 3). Without the proposal, they are categories it does not know.

Reference: commandSchema in src/core/model/model.ts (a union of 91 kinds keyed by kind).

2. The command set

The reference vocabulary has 91 kinds. They fall into core record operations, which every command processor MUST implement for the core records, and domain operations, which a processor MAY implement as a vocabulary profile.

2.1 Core record operations

KindDoesPrecondition
createadds an element; its id MUST be newexpectedHostRevision (optional)
updatepatches an element (section 4)expectedRevision
deleteremoves an element and what depends on it (section 5)expectedRevision
type.upsertwrites a type wholeexpectedRevision (optional)
type.deleteremoves a type no element usesexpectedRevision
type.duplicatecopies a type under a new id and name, optionally assigning it to one elementexpectedSourceRevision
level.upsertadds or changes a level; a new level gets its own plan unless plan: falseexpectedLevel (optional)
level.deleteremoves a level with its plans; refused while anything stands on it unless moveTo names a levelexpectedLevel
material.upsertwrites a materialexpectedRevision (optional)
grid.upsertwrites a grid lineexpectedRevision (required when the grid exists)
project.rename, project.elevationOffset, project.site, project.envelope, rooms.settings, project.informationchange the project recordexpectedName, expectedOffset, expectedSite, expectedEnvelope, expectedSettings, expectedInformation
project.excavation, project.precast, project.standardsset or clear a project rulenone
original.make, original.add, original.remove, original.rename, original.dissolve, original.frame, original.edit, copy.place, copy.move, copy.remove, copy.uniquegroups placed many timesexpectedRevision on most

2.2 Documentation profile

KindDoesPrecondition
view.upsertwrites a view wholeexpectedRevision (optional)
view.updatechanges a view's settings by name; display merges into the view's ownexpectedRevision
view.deleteremoves a view; refused while a sheet, a dependent view, a callout or a report uses itexpectedRevision
view.camerasets a 3D view's camera for a sheetexpectedRevision
detail.place, detail.update, detail.remove, detail.drawplace, change, remove or draw one 2D item of a viewexpectedRevision (the view's)
scopeBox.upsert, dimensionStyle.upsert, detailFamily.upsertwrite those recordsexpectedRevision (required when the record exists)

2.3 Domain operations (the reference vocabulary)

GroupKinds
Transformsmove, rotate, mirror, offset, array, radialArray, align, split, trim, sketch.edit
Walls, columns and outlineswall.join, wall.attach, wall.detach, wall.flip, column.attach, column.detach, edge.attach
Timber framing and trussesframing.rule.upsert, floor.framing.upsert, framing.materialise, framing.update, framing.detach, trusses.layout.upsert, trusses.plan, trusses.materialise, trusses.update, trusses.detach
Systemsmep.fall, mep.downpipe
Reinforcementrebar.quick, mesh.lay
Fabricationpiece.upsert, piece.delete, joint.upsert, joint.delete
Application recordssheet.upsert, sheet.delete, titleBlock.upsert, titleBlock.delete, image.upsert, image.delete, render.upsert, render.delete, report.upsert, report.delete, deviation.upsert, deviation.delete, areaAdjustment.upsert, areaAdjustment.delete, reference.upsert, reference.delete

A domain operation is defined by the records it produces: it MUST leave the model exactly as some sequence of core record operations would. Several are written that way in the reference: framing.rule.upsert and floor.framing.upsert become a type.upsert, trusses.layout.upsert and mep.fall an update, trusses.plan a view.upsert, mesh.lay a create, rebar.quick a batch of creates, view.update and detail.draw a view.upsert.

Reference: run in src/core/model/store.ts.

3. Preconditions

A precondition names the state a command was based on; a mismatch is refused, so two writers cannot overwrite each other unseen.

  1. When a command gives expectedRevision, the target's current revision MUST equal it, or the command MUST be refused (the reference message: "Revision conflict: expected 3, current 4. Read and retry.").
  2. Upserts. For grid.upsert, scopeBox.upsert, reference.upsert, detailFamily.upsert and dimensionStyle.upsert the precondition is strict: when the record exists, expectedRevision MUST be given and MUST equal its revision; when it does not, expectedRevision MUST be absent. For the other upserts a given expectedRevision MUST equal the record's revision; material.upsert, report.upsert, deviation.upsert and areaAdjustment.upsert refuse one for a new record, and type.upsert, view.upsert, sheet.upsert and render.upsert count a new record as revision 0.
  3. Other preconditions compare the named state: expectedLevel the whole level (every member of it, those the processor does not know and extension members included: Part I, 10 and 11.2, rule 5), expectedName the project's name, expectedSite the site (or null for none), expectedHostRevision the host's revision. A state is compared as a JSON value: the order of its members and the way a number is written do not count (Part III, 6), and a member that one side has and the other lacks is a difference.
  4. Where a precondition is optional and left out, the command is applied to whatever is there. Writers SHOULD always give one.

Reference: the revision checks in run.

4. Patches

An update carries patch: the element members to write.

  1. Each member of the patch replaces the element's member of that name, whole: nested objects are not merged.
  2. null removes a member the element may lack (top, joins, taper, hostId …). The members every element has (name, typeId, levelId, offset, height, width, profile) can only be replaced, and id, category and revision cannot be patched.
  3. A patch MUST NOT say undefined (or leave out a member it means to remove): JSON drops an undefined member, so a logged command would not remove it. A processor MUST refuse a patch that holds an undefined value for a member the record has, and tell the caller to send null.
  4. A patch that changes typeId MUST name a type of the element's category.
  5. A patch that sets a width or height the element's type sets (Part II, 5.3) MUST be refused.
  6. A patch MUST NOT write the legacy members topRoofId, topCeilingId and roofCutId.
  7. After the patch, the element's revision is the old one plus one.

view.update patches a view the same way, except that display is merged into the view's own. detail.update merges its patch into one detail item, null removing a member, and the item is checked whole again.

Reference: elementPatchSchema and patchedElement in src/core/model/model.ts; refuseLostClears and the update branch of run in src/core/model/store.ts; refuseTypeSetSizes in src/core/model/typeSets.ts.

5. Deletes and what depends on them

  1. Deleting an element MUST also delete the elements that depend on it: those hosted by it (hostId) and the framing members or trusses materialised from it (frameHost). The answer names them (removedDependents).
  2. A shaft hosted by a deleted stair survives as a plain shaft: its hostId goes and its revision goes up by one.
  3. 2D items of views that name the deleted element are removed with it, and views stop hiding it.
  4. type.delete MUST be refused while a record names the type, an element or a view's assembly (a folder holds every type its records name, Part III, 12); view.delete while a sheet, a dependent view, a callout, a view reference or a report uses the view; level.delete while anything stands on the level or reaches up to it, unless moveTo names the level they move to.

Reference: dependsOn and the delete branch of run; viewDeleteRefusal in src/core/views/viewUsers.ts; deleteLevel in src/core/model/levels.ts.

6. Validation and refusal

A command is checked in three stages, and refused at the first that fails:

  1. Shape: the command matches its kind's schema (section 1).
  2. The command's own rules: its target exists, its preconditions hold, its references name records of the right kind, and the rules of its kind (a wall attaches only to a host that covers it; an opening fits its wall).
  3. The whole model: the model after the command MUST pass every rule of Parts I and II. A command that would leave the model invalid MUST be refused, even when the command itself is well formed.

A refusal MUST say, in the terms of the domain, what is wrong and SHOULD say how to fix it. In a batch, it MUST name the command by its index, kind and id.

From the reference processor, on examples/one-storey-house, after the south wall was attached (revision 1):

{
  "ok": false,
  "dryRun": true,
  "failed": 3,
  "results": [
    {"index": 0, "ok": false, "kind": "update", "commandId": "wall-south",
     "error": "Command 0 (update wall-south): Revision conflict: expected 0, current 1. Read and retry."},
    {"index": 1, "ok": false, "kind": "update", "commandId": "wall-north",
     "error": "Command 1 (update wall-north): A wall or beam requires two distinct endpoints."},
    {"index": 2, "ok": false, "kind": "update", "commandId": "window-south",
     "error": "Command 2 (update window-south): Opening must fit within its host wall: it reaches 10500 mm along a wall 10000 mm long."},
    {"index": 3, "ok": true, "kind": "update"}
  ]
}

Reference: validateWhole and validateManifest; batchError and dryRunReport in src/core/model/store.ts.

7. Batches, atomicity and dry runs

  1. A batch is an ordered list of commands applied as one: each command sees the model as the commands before it left it, and the whole model is checked once after the last. If any command or the final check fails, the batch MUST change nothing.
  2. One command is a batch of one.
  3. A dry run (dryRun: true) MUST run every check a real run does and MUST change nothing. A dry run of a batch that fails SHOULD answer for every command: each is tried in order on a copy, a failing one is reported and left out, and the rest go on, so that the caller learns everything it has to fix at once.
  4. A batch that changes the model is one undo step.
  5. A generic element is moved to a category that fits by a batch of a delete and a create with the same id (Part II, 6.6, rule 7); no command kind does it alone.

Reference: dispatchBatch (a preview store, all or nothing) and dryRunReport in src/core/model/store.ts; execute_batch in src/automation.ts.

8. Determinism and replay

  1. Applying a command MUST be a function of the model and the command alone.
  2. A command MUST be applied as its JSON text would be stored: what JSON cannot hold (undefined members, NaN, infinities, dates) is resolved before the command runs, so the processor that sends a command and one that replays it later get the same model.
  3. A command MUST NOT mint a random id or read the clock. An id a command makes is named by the caller or derived from its input: the reference derives <host>:bars, then <host>:bars-2, -3 and so on.
  4. A processor that replays commands it has already accepted (opening a project from a log) MUST NOT refuse them by a rule made after they were accepted. Admission rules apply to new commands; application is deterministic.

Reference: asLogged and madeId in src/core/model/store.ts; replay (the replaying flag lifts the newer refusals, such as type-set sizes and edits of placed copies, for accepted commands).

9. The answer

A command processor answers an accepted command or batch with at least:

MemberMeaning
oktrue
dryRunwhether anything was committed
affectedthe ids of every record the command or batch changed, including those changed as a consequence (joined walls, dependents)
verifyhow to check the result: a filter that selects exactly the affected elements (Part VII), and the views and snapshots to look at
sequencethe host's change sequence after the command
warningswhat the caller should know but that did not refuse the command (optional)

and, per command, its result: the record written, or for a delete {deleted, removedDependents?}.

From the reference processor, renaming the west wall:

{
  "ok": true,
  "dryRun": false,
  "sequence": 1,
  "affected": ["wall-west"],
  "verify": {
    "filter": {"elementIds": ["wall-west"]},
    "snapshot3d": {"filter": {"elementIds": ["wall-west"]}, "corners": ["SW", "NE"], "hideOthers": false},
    "snapshot2d": [{"viewId": "view-plan-0", "filter": {"elementIds": ["wall-west"]}}]
  }
}

Reference: the execute and execute_batch actions and verifyHints in src/automation.ts.

10. The log (informative)

The reference implementation keeps a cloud project as a snapshot plus an append-only log of accepted batches, each stored as JSON and sequenced with a compare-and-swap; opening a project replays the log (section 8). 0.1 does not specify the log's format. A common command envelope is planned for 0.2 (decided 11 October 2026). The plan proposes it around each batch: an operation id for idempotency, attribution per person and per agent, a target branch and base, a mode (apply, dryRun, propose) and an intent text. Part XVII, proposed for 0.2, proposes it in full, with what a processor that keeps a log records of each operation.

11. A supplier's parts

This section says what a supplier's parts (Part II, 12) ask of commands.

  1. No new command kind. A supplier writes a type with declared geometry, or with the railing members of Part II, 12.2, by type.upsert; places or changes elements by create and update, where a patch of declaredGeometry or of railing replaces it whole (section 4); and materialises a railing's post positions by an update that writes railing.posts.at.
  2. Transforms carry declared geometry. move, rotate, mirror, array, radialArray and the other transforms MUST move, turn, mirror or copy an element's declared geometry with the element (Part II, 12.1, rule 6).
  3. A proposal's quote is not a command. It travels with the proposal on the host, and no command writes it into the model, nor into a proposal file (Part VI, 8.7).

Reference: mappedDeclaration in src/core/model/declaredGeometry.ts, called by the transforms in src/core/geometry/transforms.ts and src/core/geometry/modifications.ts.