# 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 of 0.2 knows the categories 0.2 adds, in every model of 0.2: 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 the rules of the part that adds them: it holds each to that part's schema, refuses what those 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 by them (stage 3). In a model of 0.1, read by 0.1's rules (Part I, 9.6, rule 3), they are a later version's categories, which it does not know, unless it is asked for the proposal of 0.1 that adds them.

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

*Note (informative).* The reference processor differs on both points. It refuses an unknown member of an element, a patch or a view (their schemas are strict: "Unrecognized key(s) in object"), against rule 3; and it drops an unknown member at the top level of a command, or of a type, without a word, against rules 3 and 4. Since Stomra pull request #778 (live since 11 October 2026, `main` `8ebedf05`) it makes one exception, and keeps a record's own: a member that a record of the model holds with the same value, carried back in a command as it was read, is not refused, and a record a command changes keeps the members it holds. So of a command that would add an extension member whose value is `null` (rule 3), it refuses one in an element, a patch or a view, as it refuses any member it does not know there, and drops one in a type, against rule 3. `@openeik/tools` refuses it, and keeps one carried back as it was read. See the open questions.

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

| Kind | Does | Precondition |
|---|---|---|
| `create` | adds an element; its id MUST be new | `expectedHostRevision` (optional) |
| `update` | patches an element (section 4) | `expectedRevision` |
| `delete` | removes an element and what depends on it (section 5) | `expectedRevision` |
| `type.upsert` | writes a type whole | `expectedRevision` (optional) |
| `type.delete` | removes a type no element uses | `expectedRevision` |
| `type.duplicate` | copies a type under a new id and name, optionally assigning it to one element | `expectedSourceRevision` |
| `level.upsert` | adds or changes a level; a new level gets its own plan unless `plan: false` | `expectedLevel` (optional) |
| `level.delete` | removes a level with its plans; refused while anything stands on it unless `moveTo` names a level | `expectedLevel` |
| `material.upsert` | writes a material | `expectedRevision` (optional) |
| `grid.upsert` | writes a grid line | `expectedRevision` (required when the grid exists) |
| `project.rename`, `project.elevationOffset`, `project.site`, `project.envelope`, `rooms.settings`, `project.information` | change the project record | `expectedName`, `expectedOffset`, `expectedSite`, `expectedEnvelope`, `expectedSettings`, `expectedInformation` |
| `project.excavation`, `project.precast`, `project.standards` | set or clear a project rule | none |
| `original.make`, `original.add`, `original.remove`, `original.rename`, `original.dissolve`, `original.frame`, `original.edit`, `copy.place`, `copy.move`, `copy.remove`, `copy.unique` | groups placed many times | `expectedRevision` on most |

### 2.2 Documentation profile

| Kind | Does | Precondition |
|---|---|---|
| `view.upsert` | writes a view whole | `expectedRevision` (optional) |
| `view.update` | changes a view's settings by name; `display` merges into the view's own | `expectedRevision` |
| `view.delete` | removes a view; refused while a sheet, a dependent view, a callout or a report uses it | `expectedRevision` |
| `view.camera` | sets a 3D view's camera for a sheet | `expectedRevision` |
| `detail.place`, `detail.update`, `detail.remove`, `detail.draw` | place, change, remove or draw one 2D item of a view | `expectedRevision` (the view's) |
| `scopeBox.upsert`, `dimensionStyle.upsert`, `detailFamily.upsert` | write those records | `expectedRevision` (required when the record exists) |

### 2.3 Domain operations (the reference vocabulary)

| Group | Kinds |
|---|---|
| Transforms | `move`, `rotate`, `mirror`, `offset`, `array`, `radialArray`, `align`, `split`, `trim`, `sketch.edit` |
| Walls, columns and outlines | `wall.join`, `wall.attach`, `wall.detach`, `wall.flip`, `column.attach`, `column.detach`, `edge.attach` |
| Timber framing and trusses | `framing.rule.upsert`, `floor.framing.upsert`, `framing.materialise`, `framing.update`, `framing.detach`, `trusses.layout.upsert`, `trusses.plan`, `trusses.materialise`, `trusses.update`, `trusses.detach` |
| Systems | `mep.fall`, `mep.downpipe` |
| Reinforcement | `rebar.quick`, `mesh.lay` |
| Fabrication | `piece.upsert`, `piece.delete`, `joint.upsert`, `joint.delete` |
| Application records | `sheet.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 `create`s, `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`.

*Note (informative).* In the reference vocabulary, preconditions are required on 51 kinds, optional on 26 and absent on 14 (`image.upsert`, `image.delete`, `project.excavation`, `project.precast`, `project.standards`, `rebar.quick`, `mesh.lay`, `mep.downpipe`, `trusses.plan`, `render.delete`, `original.make`, `copy.place`, `original.frame`, `original.edit`). A precondition on every command is 0.2's (decided 11 October 2026): rule 4 becomes a requirement in an envelope, and the kinds that had none get one. [Part XVII](https://openeik.org/spec/0.2/envelope), of 0.2, says how: in an envelope every command gives its kind's precondition, an upsert `null` for a record that does not exist yet ([9.1](https://openeik.org/spec/0.2/envelope#9.1)), and each of the fourteen kinds gets one ([9.2](https://openeik.org/spec/0.2/envelope#9.2)).

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

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

*Note (informative).* The reference refusals are text. Machine-readable error codes are 0.2's (decided 11 October 2026); the plan proposed codes such as `EIK-WALL-012`, JSON pointers to the member that is wrong, and a separate `fix`. [Part XVII, 10](https://openeik.org/spec/0.2/envelope#10), of 0.2, gives them: the validator's codes rather than numbered ones, so that a rule has one code whether a command is refused for it or a folder reported, each refusal with its rule, a JSON pointer into the command, a message and a fix where one is known, and a catalogue of the codes.

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

| Member | Meaning |
|---|---|
| `ok` | `true` |
| `dryRun` | whether anything was committed |
| `affected` | the ids of every record the command or batch changed, including those changed as a consequence (joined walls, dependents) |
| `verify` | how to check the result: a filter that selects exactly the affected elements (Part VII), and the views and snapshots to look at |
| `sequence` | the host's change sequence after the command |
| `warnings` | what 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:

```json
{
  "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 0.2's (decided 11 October 2026). The plan proposed 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](https://openeik.org/spec/0.2/envelope), of 0.2, gives 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`.

*Note (informative).* The reference processor accepts and checks declared geometry and the railing members since Stomra pull requests #786 and #787 (live since 12 October 2026, `main` `1e1089db`).
