OpenEik.org

Draft OpenEik 0.4.0: draft, not final. From commit 9077aa6c of 1 November 2026. Errata: the errata of 0.4. Raw text: /spec/0.4/envelope.md. Earlier: OpenEik 0.1, OpenEik 0.2, OpenEik 0.3, kept as they were.

On this page

OpenEik 0.4 specification

Part XVII. Commands with an envelope

This part is a part of OpenEik 0.2. It carries out what the steward decided on 11 October 2026 for 0.2 (open question 5): a common command envelope around every command or batch, machine-readable error codes for refusals, and a precondition on every command (Part V, 3, 6 and 10). It binds every conformance class it names (section 13; Conformance). OpenEik 0.1 had it as a proposal, which no class of 0.1 required; a batch of 0.1 stays valid (section 8). Its own questions, open questions 53 to 60, the steward decided on 25 October 2026, each as the proposal had it (section 14). @openeik/tools applies it to every model of 0.2, and to a model of 0.1 when asked, since 25 October 2026 (section 13).

1. Status and scope

Part V says what a command is, how a processor checks it and what it answers. Part VII says how an agent reaches a processor. Neither says who sent a batch, what it was made from, whether it is a second try of one already applied, or what is wrong with a refused command in terms a program can act on. An agent that times out cannot tell whether its batch was applied; a host cannot tell an agent's write from its person's; a refusal is a sentence.

This part says it. A writer sends its batch in an envelope: an id for the operation, who sends it, the line of work and the version the commands were made from, what to do with them, and why, in words. A processor answers every refusal with a code, the member of the command that is wrong, a message and, where one is known, a fix. And every command names the state it was made from.

What it leaves out:

  • Transport. How an envelope travels (an HTTP request, an MCP tool call, a file) is the host's. OpenEik still defines no protocol between stores (Part VI, 6; open question 28).
  • Accounts. Who a person is, and which accounts and agents may act for them, is the host's. The envelope names them; the host checks them (section 4).
  • The log. Part V, 10 leaves the format of a log of accepted batches open, and so does this part, beyond what a processor records of an operation (section 3).
  • Undo across writers, and change events. An undo that can be rebased, and events that say why a record changed, are later work.

1.1 The parts it extends

In 0.2 its text stays in this part. These are the sections of Parts I to VIII and of the other parts that it extends, where a later version may move it:

  • Part V: the envelope as a section of its own; the codes into 6 and 9; rule 4 of 3 made a requirement, and the fourteen kinds' preconditions into the tables of 2;
  • Part VI: the agent beside the author in a version (3), and a proposal made from an operation (8.7);
  • Part VII: attribution and the codes in the write loop (6), in place of 10's note;
  • Conformance: what a command processor and an agent host do with an envelope;
  • Security and privacy: attribution as personal data, an agent's identity, and operation ids.

2. The envelope

An envelope is a JSON object, held to schemas/0.2/envelope/envelope.schema.json:

MemberMeaning
formatrequired"openeik-envelope/0.1"
operationIdrequiredthe operation's id (section 3)
byrequiredwho sends it: {person, agent?, app?} (section 4)
targetrequiredthe line of work and the version the commands were made from: {ref, version, exact?} (section 5)
moderequiredapply, dryRun or propose (section 6)
intentrequiredwhat the change is for, in words for people (section 7)
commandsrequiredthe batch: one or more commands of Part V, in order

An agent renames the west wall of examples/one-storey-house, read at a version of main:

{
  "by": {"agent": {"name": "an-agent", "session": "s-41c2", "version": "2.1"}, "app": "an-app 1.0", "person": "u-7f3c"},
  "commands": [
    {"expectedRevision": 1, "id": "wall-west", "kind": "update", "patch": {"name": "West wall (render)"}}
  ],
  "format": "openeik-envelope/0.1",
  "intent": "Name the west wall by its finish, as the client asked",
  "mode": "apply",
  "operationId": "0b6f6a4e-2c1d-4f7e-9a51-3d2b8c7e1f00",
  "target": {"ref": "main", "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d"}
}
  1. A processor MUST refuse an envelope whose format it does not implement (envelope-format), and one that does not validate against its schema (envelope-schema), naming the member that is wrong. It applies none of its commands.
  2. A member the envelope does not define MUST be refused (envelope-schema), but an extension member (<prefix>:<name>, Part I, 11.2), which is kept and has no meaning here. A misspelt member of an envelope changes what happens to every command in it: operationID would lose the operation's id, and exacts the writer's wish.
  3. The commands are a batch of Part V, 7: applied in order, all or nothing, and checked as Part V, 6 says, each with its kind's precondition (section 9).
  4. Nothing in an envelope but its commands changes the model. Its other members say whether, where and for whom the commands are applied, and what is recorded beside them (section 4.3).

3. The operation

An operation is one envelope, and what it does. Its id lets a writer send it again, after a lost answer or a timeout, without its commands being applied twice.

  1. The id. operationId is 8 to 128 ASCII letters, digits and ., _, : and -, starting with a letter or a digit. A writer makes one for each operation and SHOULD make it a UUID (RFC 9562, version 4 or 7). It MUST NOT give two operations one id, and MUST NOT put personal data into an id.

  2. The same operation. Two envelopes with one id are one operation sent twice where their digests are equal: the SHA-256, as 64 lowercase hexadecimal digits, of the envelope's canonical JSON (Part III, 6), every member included. A writer that sends an operation again sends the same envelope.

  3. A processor that keeps a log of what it applies (a host, a version store, an agent host that keeps a folder) MUST record each operation it applies, by its id, with its digest and its answer, together with the change it applied: in one transaction with the log entry, the version or the ref's move, so that it never holds the change without the record, nor the record without the change. It MUST keep the record for at least 24 hours after the operation, and SHOULD keep it as long as it keeps the change's log entry. For an id it has recorded, it MUST then:

    • for an envelope with the same digest, answer with the recorded answer, marked as such (operation.repeated: true), and apply nothing;
    • for an envelope with another digest, refuse it (operation-reused) and apply nothing.

    For an id whose operation it is still applying, it MUST refuse the second (operation-in-progress), and apply nothing; the writer sends it again later.

  4. What is recorded. An operation applied is recorded, and so is a proposal a host makes (6.3, rule 2), since each changes something. A refused operation, a dry run and a proposal file (6.3, rule 1) change nothing on the target, and are not recorded: the writer may send the same id again, with the same envelope or a mended one.

  5. A processor that keeps no log (a library that applies commands to a model it is given) cannot tell a second try from a first. It MUST say so in its answer (operation.kept: false), and applies the operation as a new one. In an envelope every command gives its precondition (section 9), so an operation applied before is refused the second time, with a revision-conflict or an id-duplicate, never applied twice; the writer reads the records again and sees whether its first try was applied.

  6. An operation's id is no part of the model, nor of a version (Part VI, 3): it is the processor's record of what was sent, and a squashed push makes one version of many operations (Part VI, 6.3, rule 3).

4. Attribution

4.1 Who sends it

by says for whom the operation is made, which agent made it, if one did, and the application it came through:

MemberMeaning
personrequiredthe person the operation is made for, by an id the processor knows them by
agentwhere an agent made it{name, version?, session?}: the agent, as it names itself, its version, and the session it works in, as the host named it
appoptionalthe application the operation came through, with its version, as a proposal file's by.app names one ("an-analysis-app 0.5.0")
  1. person is an id: on a host, the id of the person's account; in a folder, a person's id of Part XIV, 5, or the id the person gives. It MUST NOT be an email address (Security and privacy, 2.3).
  2. An operation that an agent made, on a person's word or on its own, MUST name the agent. One that a person made in an application's own interface names none.
  3. An agent writes for a person: person is the person it works for, never the agent. An agent that works for no one in particular (a scheduled check) writes for the person who set it to work.

4.2 What a host checks

  1. A host MUST authenticate the writer of an envelope, and MUST refuse one whose by.person is neither the person it authenticated nor one that person may act for by the host's rules (person-mismatch).
  2. What an agent says of itself proves nothing: its name, version and session are its words. A host that can tell an agent's credential from a person's, such as a token issued to a client other than the host's own pages, MUST record an operation sent with an agent's credential as an agent's, naming the client it checked, even where the envelope names no agent; and MUST NOT record such an operation as the person's own.
  3. An agent writes with the rights of the credential it holds, at most the person's. A host MAY give it fewer: it may refuse an agent's apply to a line of work a person edits (6.4).

4.3 What is recorded

  1. A version. A version made from operations (Part VI, 3) has the person as its author. In 0.2 a version's commit object has an optional member agents: each agent whose operations it holds, as {name, version?}, once each, sorted as their canonical JSON is by UTF-16 code units; a version that holds none has no agents. A version of 0.1 has no agents, and a host names the agent in its note (Security and privacy, 3.3).
  2. A proposal file made from an operation (6.3) has by.person the envelope's, by.app the envelope's app, or else the agent's name and version, and intent the envelope's. In 0.2 its by has agent, as the envelope gives it.
  3. A log. A processor that keeps a log of accepted batches SHOULD record beside each its operation's id and digest, by (and the client it checked, 4.2, rule 2), the target, the mode and the intent.

5. The target

target names the line of work the commands are for, and the version of it they were made from:

MemberMeaning
refrequireda ref of Part VI, 3: main, alternatives/<slug>, or another the processor keeps
versionrequiredthe version the writer read and made the commands from, the base: a version's id (Part VI, 3), a git commit's id for a folder kept in git, or null where the writer read no version
exactoptionaltrue: apply to the base only
  1. A processor MUST refuse a ref it does not keep, or one it does not let the writer write (target-unknown). A processor that keeps one line of work, a folder or a session, calls it main.
  2. Where the ref's newest version is the base, or the base is null, the commands are applied to the ref's newest version.
  3. Where the ref has moved on from the base:
    • with exact: true, the processor MUST refuse the operation (base-moved), naming the ref's newest version, and apply nothing;
    • otherwise, it applies the commands to the ref's newest version, and their preconditions decide (section 9): a command whose record changed since the base is refused, and the others are applied to the model as it is now. The answer says that the ref had moved (target.moved).
  4. A processor that keeps versions MUST move the ref from the version it applied the commands to, by compare-and-swap (Part VI, 3, rule 3). Where the swap fails, another write having come in between, it applies the commands again to the newer version by rule 3, or refuses them where the envelope asks for its base exactly.
  5. For the mode propose, the base is what the proposal is made against (6.3).
  6. A processor that keeps no versions, such as a library applying commands to a model it is given, compares the base only with a version its caller names, and says whether it compared them (target.compared).

6. The mode

6.1 Apply

The batch is applied as Part V, 7 says, all or nothing. Accepted, the change is made, the ref moves (section 5) and the operation is recorded (section 3).

6.2 Dry run

Every check a real run makes is made, and nothing changes (Part V, 7.3). A failing dry run answers for every command. It is 0.1's dryRun: true, and is not recorded.

6.3 Propose

Nothing changes on the target. The processor checks the batch as a dry run, on the base where it keeps versions, else on the model it has. Where the batch passes it makes a proposal of it, and where it fails, none. The proposal is one of:

  1. a proposal file (Part VI, 8.7) that an application keeps in its folder: format, the envelope's intent, base (the envelope's base version, and each record the commands change as it is there, by its hash and revision), by (4.3, rule 2), the commands, and the dry run that passed; or
  2. a host's proposal: the commands applied on a line of work of its own, made from the base for the proposal, and offered for merging into ref, with the envelope's intent and by. The answer names it (section 11).

A person accepts a proposal, as Part VI, 8.7, rules 4 and 5 say.

A suggestion, a change that a party offers to records another party owns, is such a proposal, with no form of its own (Part XVI, 5, rule 5; decided on 30 October 2026).

6.4 What a processor may refuse

  1. A host MAY refuse an apply to a ref it holds for review, such as an agent's to a line a person edits (Part VI, 8.7, rule 5), with mode-not-allowed, and SHOULD say that it takes a proposal.
  2. A processor MUST NOT make a proposal of an apply, nor apply a propose: the writer chose the mode.
  3. A processor that implements this part implements apply and dryRun. One that does not implement propose refuses it (mode-not-implemented).

7. The intent

intent is what the change is for, in a sentence a person reads first: "Name the west wall by its finish, as the client asked."

  1. It is plain text of 1 to 1000 characters. Nothing in it is markup, a link or an instruction: a host shows it as text, and an agent that reads it reads it as data (Security and privacy, 3.1).
  2. It is the writer's account of the change. What the change does is what its commands do.
  3. It becomes a proposal's intent (6.3), and a processor that keeps a log SHOULD record it with the operation (4.3, rule 3).

8. A batch without an envelope

  1. A batch of 0.1 stays valid: a list of commands, or one command, without an envelope. A processor that implements this part MUST still take one as Part V says, its mode apply or a dry run as the processor's own flag says, its target the line the processor is on, its base unread, and with no operation: it is not recorded, and a second try is told from a first only by its preconditions. Its commands need not give their preconditions (Part V, 3, rule 4 stays a SHOULD for them), and its refusals carry codes (section 10).
  2. An object whose format begins with openeik-envelope/ is an envelope; any other object is a command, and a list is a batch of commands.
  3. A host MAY take a writer's commands only in an envelope, and refuses a bare batch then (envelope-required): it may ask that of agents, and of the writers it does not run itself. A host that lets agents write SHOULD take an agent's commands only in an envelope, so that every agent's write is attributed and can be sent again safely; a person's own application may go on sending bare batches.

9. A precondition on every command

9.1 Rule 4 made a requirement

In an envelope, every command MUST give its kind's precondition, which Part V, 3 lists and 9.2 adds to, and a processor MUST refuse one that gives none (precondition-missing), naming the member it lacks. The precondition names the state the command was made from, so a command made from a record that has changed since is refused, not applied over the change.

  1. An upsert names a record that does not exist yet with null: expectedRevision: null for type.upsert, material.upsert, grid.upsert and the upserts of the documentation profile and the domain operations, and expectedLevel: null for level.upsert. A processor MUST refuse an upsert with null of a record that exists, as a conflict, and one with a revision of a record that does not, as a precondition with nothing to name (precondition-target). In 0.1 a type.upsert, view.upsert, sheet.upsert or render.upsert of a new record may say revision 0; in an envelope it says null, so that a number always names a record that is there.
  2. A create gives expectedHostRevision where its element has a host. One without a host needs nothing more: that its id is new is its precondition (Part V, 2.1).
  3. A precondition that compares a state (expectedSite, expectedEnvelope, expectedSettings, expectedInformation, expectedExcavation, expectedPrecast, expectedStandards) gives the state as read: null where there was none, {} for rooms' settings and project information that were empty.
  4. A command inside another, as in original.edit, gives its own precondition too.
  5. A replay of commands a log accepted is not refused for a precondition it lacks (Part V, 8.4): the rule is newer than they are.

9.2 The fourteen kinds

Part V, 3's note lists fourteen kinds of the reference vocabulary that take no precondition in 0.1. Each gets one:

KindPreconditionIt names
project.excavationexpectedExcavationthe project's excavation as read, or null where it had none
project.precastexpectedPrecastthe project's precast as read, or null
project.standardsexpectedStandardsthe project's standards as read, or null
image.upsertexpectedHashthe hash (Part III, 6) of the image's record as read, or null for a new image; an image has no revision, and its record holds its data, which the hash names without sending it back
image.deleteexpectedHashthe hash of the image's record as read
render.deleteexpectedRevisionthe render's revision
rebar.quickexpectedHostRevisionthe revision of the host the bars are worked out from
mesh.layexpectedHostRevisionthe revision of the host the mesh is laid on
mep.downpipeexpectedGutterRevisionthe revision of the gutter the downpipe is worked out from
trusses.planexpectedRevisionthe revision of the plan view it writes, or null for a new one
original.makeexpectedRevisions{"<element id>": <revision>, …}: the revision of each element of elementIds, and of no other
original.frameexpectedRevisionthe group's revision
original.editexpectedRevisionthe group's revision; each command of the edit gives its own as well
copy.placeexpectedOriginalRevisionthe revision of the group it places (copy.of)

Their schemas are in $defs/preconditions of the envelope's schema, and the member each kind takes is in the schemas' index, rules.envelope.preconditions of schemas/0.2/index.json. A precondition of 9.2 compares as Part V, 3 says: a revision with the record's, a state as a JSON value, a hash with the record's hash.

10. Refusals with codes

10.1 The form of a code

A code is a lowercase name of words joined by hyphens, as the validator's codes are (revision-conflict, opening-fit): ^[a-z][a-z0-9]*(-[a-z0-9]+)*$. A processor's own code, for a rule this specification does not state, carries its prefix (Part I, 11.1): acme:sliver.

The plan proposed codes such as EIK-WALL-012: a namespace, a domain and a number. This part takes the validator's form instead, for three reasons:

  1. One rule, one code. The validator of @openeik/tools names every finding with a code of this form, more than 200 of them, which the conformance suite's cases name. A refusal for a rule the validator checks carries that code (10.5), so a command refused for an opening that does not fit and a folder reported for one say opening-fit alike, and a program that handles one handles the other. Numbered codes would have been a second name for each rule.
  2. A code that says what it is. An agent reads revision-conflict and knows to read again; EIK-CMD-003 it has to look up. The rule a refusal breaks travels beside its code (rule: "V.3.1"), so a code needs no numbers to be found in the text.
  3. Not by domain. Most refusals are of no one category: a conflict, a missing target, a member a kind does not define. Codes grouped by domain would give each a home it does not have.

10.2 The refusal

A refused command's answer (Part V, 9) holds its refusals. Each is an object:

MemberMeaning
coderequiredthe code (10.1)
rulerequiredthe rule it breaks: a Part's number, the section and the rule, as V.3.1 or II.6.1.5
pointerrequireda JSON pointer (RFC 6901) into the command, at the member that is wrong: /expectedRevision, /patch/profile; "" for the whole command
messagerequiredwhat is wrong, for people, in the terms of the domain (Part V, 6)
fixoptionalwhat would make the command pass, where it is known (10.3)
dataoptionalvalues for a program: a conflict's expected and current, the records that use what a delete would remove (users)
recordoptionalwhere in the model the refusal is: {list, id, pointer}, a record and a pointer in it
  1. A processor MUST give each refusal a code, a rule, a pointer and a message. A command may have several refusals: the whole model's check (Part V, 6.3) may find several rules broken after it.
  2. The pointer is into the command as sent. A refusal of a command inside another (original.edit) points into the outer command: /commands/0/expectedRevision. A refusal of the envelope itself points into the envelope (/target/version), and one that the whole model's check finds points at the member of the command that wrote the record, where the command is an update whose patch gives it or a create of the record, and at the whole command otherwise, with record naming where the rule is broken.
  3. The message is the one Part V, 6 asks for: in a batch it names the command by its index, kind and id (the answer's error), and its refusals give their messages without that lead.

From @openeik/tools, on examples/one-storey-house, the second command of Part V, 6's example:

{"code": "profile-short", "message": "A wall or beam requires two distinct endpoints.", "pointer": "/patch/profile", "rule": "II.6.1.2"}

and its first:

{
  "code": "revision-conflict",
  "data": {"current": 1, "expected": 0},
  "fix": {"read": [{"id": "wall-south", "list": "elements"}]},
  "message": "Revision conflict: expected 0, current 1. Read and retry.",
  "pointer": "/expectedRevision",
  "rule": "V.3.1"
}

and its third, which the whole model's check finds after it, on the window the patch moved:

{
  "code": "opening-fit",
  "message": "Opening must fit within its host wall: it reaches 11250 mm along a wall 10000 mm long.",
  "pointer": "/patch/along",
  "record": {"id": "window-south", "list": "elements", "pointer": "/along"},
  "rule": "II.6.1.5"
}

10.3 Fixes

A fix says what would make the command pass, where the processor knows it. It is one of:

  • {"patch": [...]}: a JSON Patch (RFC 6902) of the command, such as {"op": "move", "from": "/expectedRevison", "path": "/expectedRevision"} for a misspelt member, or {"op": "replace", "path": "/element/width", "value": 1200} for a size the type sets;
  • {"command": {...}}: a command to send in the command's place;
  • {"read": [{"list": ..., "id": ...}, ...]}: the records to read again, and make the command from.
  1. A fix changes nothing by itself. A writer that takes it sends a new command, checked as any is.
  2. A fix MUST NOT write a precondition the writer did not read: the fix of a conflict is a read, never the command with the current revision written in, which would apply it over the change the precondition guards against.
  3. A fix is a suggestion. Where several would do, or the choice is the person's (where the elements of a level that is deleted go), a processor gives none, and its message says what the choices are.

10.4 The catalogue

These are the codes of a command processor's refusals that are its own. @openeik/tools lists them as COMMAND_CODES. The messages are the reference implementation's, or @openeik/tools's where the reference has none.

The command's shape (Part V, 1, 4 and 8; stage 1 of Part V, 6):

CodeRuleRefused whenPointerFixMessage (reference)
command-not-objectV.1.1a command is no JSON object""
kind-missingV.1.1a command names no kind, as a string/kind
kind-unknownV.1.2its kind is none of the vocabulary's/kind
kind-not-implementedV.2its kind is of a profile or of the reference vocabulary that the processor does not implement/kind
command-member-unknownV.1.4it holds a member at its top level that its kind does not definethe membera move to the member meant, where each is a misspelling"Unrecognized key(s) in object", with "did you mean"
command-schemaV.1.2it does not match its kind's schemathe first member that does notthe schema's words, by path
command-not-jsonV.8.2it holds what JSON cannot: NaN, an infinity, a BigInt, itselfthe first such value
command-too-deepV.8.2it nests deeper than any record maywhere it does
patch-undefinedV.4.3a patch says undefined for a member its record hasthe memberadd it as null"The patch says undefined for …. … Send null to take a field away."
patch-fixedV.4.2a patch writes an element's id, category or revisionthe member
patch-legacyV.4.6a patch writes topRoofId, topCeilingId or roofCutIdthe member
patch-removesV.4.2a patch takes away, with null, a member every element hasthe member

The command's own rules (Part V, 3 and 5; stage 2 of Part V, 6):

CodeRuleRefused whenPointerFixMessage (reference)
target-missingV.6.2the record the command changes, deletes, copies or names as its target is not thereits id"Element not found.", "Type not found.", "Level no longer exists."
element-of-copyII.10the element it names is a placed copy's, worked out and never storedits id
revision-conflictV.3.1a revision precondition does not holdthe preconditionread the record"Revision conflict: expected 3, current 4. Read and retry."
state-conflictV.3.3a state precondition does not holdthe preconditionread the record"Project name changed since it was read. Read and retry.", "Level conflict. Read the current level before updating."
precondition-missingV.3.2a precondition is required and not given: a strict upsert of a record that exists, or any command in an envelopethe member it lacksread the record"Grid … exists (revision 2): give expectedRevision 2 to change it."
precondition-targetV.3.2a precondition names a record that is not there: a revision of a new record, a host's revision for an element without a hostthe preconditionleave it out, or in an envelope null
in-useV.5.4a delete is refused while records use what it would remove: a type, a level and its plans, a viewthe target"Type … is used by 2 elements (…). Reassign them …"
level-lastII.3it would remove the last levelthe target
level-move-toV.5.4a level's delete names in moveTo no other level/moveTo
group-memberII.10it changes an element of a group other than in the group's editthe element"… is part of the group G1 (instance G1). Edit the group to change it …"
group-by-handII.10it writes original, copy or born itselfthe member
group-twiceII.10an element, or one that stands in it, is of another group alreadythe element"… is of the group G1 already. An element is in one group."
group-not-memberII.10it takes out of a group an element not of itthe element
group-hostedII.10it takes out of a group an element that stands in another of the group, without that elementthe element
group-instancesII.10a group is dissolved while it has other instancesthe group
group-originII.10a group's elements have no place of their own, and no origin is given/elementIds
edit-nestedII.10an original.edit holds anotherthe inner edit
copies-not-derivedII.10it needs a copy's elements worked out, which the processor does not derivethe instance

The envelope (sections 2 to 8, and 12):

CodeRuleRefused whenPointer
envelope-schemaXVII.2the envelope does not match its schema, or holds a member it does not definethe member
envelope-formatXVII.2it names a format the processor does not implement/format
envelope-requiredXVII.8the host takes this writer's commands only in an envelope""
operation-reusedXVII.3an operation id the processor recorded comes with another envelope/operationId
operation-in-progressXVII.3an operation id comes again while its first is being applied/operationId
person-mismatchXVII.4.2by.person is not the person the host authenticated, nor one they may act for/by/person
target-unknownXVII.5the target's ref is not one the processor keeps, or not one the writer may write/target/ref
base-movedXVII.5the ref has moved since the base, and the envelope asks for its base exactly/target/version
mode-not-implementedXVII.6.4the mode is one the processor does not implement/mode
mode-not-allowedXVII.6.4the host does not let this writer apply to this ref/mode
not-permittedXVII.12the writer may not change the target, or a record the commands changethe command, or ""
too-largeXVII.12the envelope holds more commands or bytes than the processor takes in one/commands

A processor MAY refuse for a rule this specification does not state, with a code of its own under its prefix: the reference implementation's refusals for a pinned element ("Unpin the element before moving, reshaping or deleting it."), a sliver thinner than 0.1 mm, a type's name taken in its category, or walls that cannot join as a T would be stomra:pinned, stomra:sliver, stomra:type-name-taken and stomra:join-tee until a later version names their rules.

10.5 The validator's codes

  1. A refusal for a rule the validator checks carries the validator's code for that rule, the code @openeik/tools gives the same finding in a folder (its CODES). A command's own stage refuses with these where its rule is the validator's: id-duplicate (I.6.1), ref-missing (I.7), type-category (I.7.1), host-category (II.6.1), profile-short (II.6.1.2), type-size (II.5.3), top-host (IV.5.3.1), trim-host (II.6.7.2), railing-host (II.12.2.2), railing-edges (II.12.2.3), generic-shape (II.6.6.2), generic-points (II.6.1.12), category-unknown (I.10.5), extension-null (I.11.2.3) and schema-invalid (III.3.2).
  2. A batch the whole model's check refuses (Part V, 6.3) gives, for each error the batch brought into the model, the validator's code and rule, the command after which it appeared (Part V, 7.1), and the record it is on. An error the model had before the batch is no command's: the answer gives it apart (section 11), with its code.
  3. Its severity is a refusal's: a code that the validator gives as a warning, such as category-unknown, is a refusal where a command would break the rule (a command makes no record of a category it does not know, Part V, 1.5).
  4. No code is both a processor's own and a validator's.

11. The answer

The answer of Part V, 9, held to schemas/0.2/envelope/answer.schema.json, gains:

MemberMeaning
modealwaysthe mode the batch was run in
operationfor an envelope{id, digest, kept, repeated?}: the operation's id and digest, whether the processor records operations (section 3), and repeated: true where this answer is the recorded one, given again
targetfor an envelope{ref, base, version, compared, moved?}: the target's ref and base, the version the commands met (null where the processor keeps none), whether the two were compared, and whether the ref had moved since the base
results[i].refusalsfor a refused commandits refusals (10.2)
refusalswhere there are anythe refusals that are no one command's: the envelope's (sections 2 to 8), and the errors the model had before the batch
proposalfor an accepted proposethe proposal file (6.3, rule 1), or the host's proposal: {id, ref, url?} (6.3, rule 2)
  1. dryRun is true for the modes dryRun and propose: nothing was committed to the target.
  2. A refused command keeps its error, the message of Part V, 6 that names it, beside its refusals.
  3. An envelope refused before its commands are tried has no results, and its refusal in refusals.
  4. A repeated operation's answer is the recorded one: it says what the first did, and nothing was applied again.

From @openeik/tools, the envelope of section 2 applied to examples/one-storey-house, with operations recorded and the model's version named as its base (verify and the wall's record shortened):

{
  "affected": ["wall-west"],
  "dryRun": false,
  "mode": "apply",
  "ok": true,
  "operation": {"digest": "9d758d99b3b57f8ccc7227500f08316cf564dbfef9916a2bb14c63c0d8caa9e3", "id": "0b6f6a4e-2c1d-4f7e-9a51-3d2b8c7e1f00", "kept": true},
  "results": [{"index": 0, "kind": "update", "ok": true, "result": {"id": "wall-west", "name": "West wall (render)", "revision": 2, "…": "…"}}],
  "target": {"base": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d", "compared": true, "moved": false, "ref": "main", "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d"},
  "verify": {"filter": {"elementIds": ["wall-west"]}, "…": "…"}
}

12. Security and privacy

  1. Attribution is personal data. by.person names a person, and an operation recorded with its by says who did what and when. It is an id, never an email address (4.1, rule 1). A processor SHOULD keep the record of operations only as long as section 3 needs it and its log keeps the change, and the agents in a version (4.3, rule 1) are kept by every store and clone that has the version (Security and privacy, 2.4).
  2. An agent's identity is its credential. What an agent says of itself, its name, version and session, is unchecked. A host decides what an agent may do by the credential it holds (4.2), records an operation sent with an agent's credential as an agent's whatever the envelope says, and MUST NOT let the envelope widen what the credential allows.
  3. Operation ids carry nothing. An id is made for the operation alone, from no personal data (3, rule 1); it is seen by whoever reads the processor's log. An id that comes back with another envelope is refused (operation-reused), so that a writer cannot be answered with another writer's result by guessing its id: a processor that keeps a log MUST keep operations apart by the person they were made for, and MUST NOT give an operation's recorded answer to a writer who is neither its person nor one that may act for them.
  4. The intent is text. It is shown as text, never as markup, and an agent reads it as data, not as an instruction (section 7; Security and privacy, 3.1).
  5. Fixes are suggestions. A writer applies a fix as a new command, checked as any is; a fix never bypasses a precondition (10.3, rule 2). An agent SHOULD NOT apply a fix that deletes or moves records without the person's word.
  6. Limits. A processor MAY refuse an envelope larger than it takes in one (too-large), and a change a writer may not make (not-permitted), such as one to records another party owns where it enforces ownership (Part XVI, 4).

13. Conformance

A command processor of 0.2 takes envelopes and bare batches (section 8), checks an envelope as section 2 says, records operations where it keeps a log (section 3), applies to its target as section 5 says, implements apply and dryRun (section 6), requires every command in an envelope to give its precondition (section 9), and answers every refusal with a code, a rule, a pointer and a message (sections 10 and 11). An agent host of 0.2 authenticates the writer and checks attribution (section 4.2).

14. The decisions

Open questions 53 to 60 (Open questions, 3) are this part's. The steward decided them on 25 October 2026, each as this part proposed it.

QuestionDecided
53The form of a code: the validator's lowercase, hyphenated names, one code for one rule whether a refusal or a finding reports it, rather than the plan's EIK-WALL-012; and the validator's codes made the standard's in 0.2 with them (10.1 and 10.5)
54A moved base: applied to the ref's newest version, the preconditions deciding, unless the envelope asks for its base exact (5, rule 3)
55The agent in the history: a version's commit object gains agents, and a proposal file's by gains agent, in 0.2 (4.3)
56A base by sequence: not yet; the base is a version or null, and a host that keeps a change sequence rather than versions relies on the commands' preconditions, until a second host asks for a sequence in target (section 5)
57How long an operation is recorded: at least 24 hours, and as long as its change's log entry where that is kept (3, rule 3)
58A person by id only: never an email address in an envelope, though a proposal file's by.person and a result manifest's made.by may hold one in 0.1 (4.1, rule 1)
59Envelopes for agents: an agent host that implements this part MUST take envelopes, and SHOULD require them of agents; a person's own application may send bare batches (section 8, rules 1 and 3)
60An envelope's own members: a member it does not define refused, rather than kept as a record's is (2, rule 2)