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:
| Member | Meaning | |
|---|---|---|
format | required | "openeik-envelope/0.1" |
operationId | required | the operation's id (section 3) |
by | required | who sends it: {person, agent?, app?} (section 4) |
target | required | the line of work and the version the commands were made from: {ref, version, exact?} (section 5) |
mode | required | apply, dryRun or propose (section 6) |
intent | required | what the change is for, in words for people (section 7) |
commands | required | the 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"}
}
- A processor MUST refuse an envelope whose
formatit 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. - 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:operationIDwould lose the operation's id, andexactsthe writer's wish. - 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).
- 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.
The id.
operationIdis 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.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.
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.- for an envelope with the same digest, answer with the recorded answer, marked as such (
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.
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 arevision-conflictor anid-duplicate, never applied twice; the writer reads the records again and sees whether its first try was applied.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:
| Member | Meaning | |
|---|---|---|
person | required | the person the operation is made for, by an id the processor knows them by |
agent | where 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 |
app | optional | the application the operation came through, with its version, as a proposal file's by.app names one ("an-analysis-app 0.5.0") |
personis 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).- 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.
- An agent writes for a person:
personis 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
- A host MUST authenticate the writer of an envelope, and MUST refuse one whose
by.personis neither the person it authenticated nor one that person may act for by the host's rules (person-mismatch). - What an agent says of itself proves nothing: its
name,versionandsessionare 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. - 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
applyto a line of work a person edits (6.4).
4.3 What is recorded
- 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 memberagents: 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 noagents. A version of 0.1 has noagents, and a host names the agent in itsnote(Security and privacy, 3.3). - A proposal file made from an operation (6.3) has
by.personthe envelope's,by.appthe envelope'sapp, or else the agent's name and version, andintentthe envelope's. In 0.2 itsbyhasagent, as the envelope gives it. - 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:
| Member | Meaning | |
|---|---|---|
ref | required | a ref of Part VI, 3: main, alternatives/<slug>, or another the processor keeps |
version | required | the 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 |
exact | optional | true: apply to the base only |
- A processor MUST refuse a
refit 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 itmain. - Where the ref's newest version is the base, or the base is
null, the commands are applied to the ref's newest version. - 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).
- with
- 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.
- For the mode
propose, the base is what the proposal is made against (6.3). - 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:
- a proposal file (Part VI, 8.7) that an application keeps in its folder:
format, the envelope'sintent,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 - 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 andby. 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
- A host MAY refuse an
applyto a ref it holds for review, such as an agent's to a line a person edits (Part VI, 8.7, rule 5), withmode-not-allowed, and SHOULD say that it takes a proposal. - A processor MUST NOT make a proposal of an
apply, nor apply apropose: the writer chose the mode. - A processor that implements this part implements
applyanddryRun. One that does not implementproposerefuses 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."
- 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).
- It is the writer's account of the change. What the change does is what its commands do.
- 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
- 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
applyor 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). - An object whose
formatbegins withopeneik-envelope/is an envelope; any other object is a command, and a list is a batch of commands. - 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.
- An upsert names a record that does not exist yet with
null:expectedRevision: nullfortype.upsert,material.upsert,grid.upsertand the upserts of the documentation profile and the domain operations, andexpectedLevel: nullforlevel.upsert. A processor MUST refuse an upsert withnullof 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 atype.upsert,view.upsert,sheet.upsertorrender.upsertof a new record may say revision 0; in an envelope it saysnull, so that a number always names a record that is there. - A
creategivesexpectedHostRevisionwhere its element has a host. One without a host needs nothing more: that its id is new is its precondition (Part V, 2.1). - A precondition that compares a state (
expectedSite,expectedEnvelope,expectedSettings,expectedInformation,expectedExcavation,expectedPrecast,expectedStandards) gives the state as read:nullwhere there was none,{}for rooms' settings and project information that were empty. - A command inside another, as in
original.edit, gives its own precondition too. - 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:
| Kind | Precondition | It names |
|---|---|---|
project.excavation | expectedExcavation | the project's excavation as read, or null where it had none |
project.precast | expectedPrecast | the project's precast as read, or null |
project.standards | expectedStandards | the project's standards as read, or null |
image.upsert | expectedHash | the 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.delete | expectedHash | the hash of the image's record as read |
render.delete | expectedRevision | the render's revision |
rebar.quick | expectedHostRevision | the revision of the host the bars are worked out from |
mesh.lay | expectedHostRevision | the revision of the host the mesh is laid on |
mep.downpipe | expectedGutterRevision | the revision of the gutter the downpipe is worked out from |
trusses.plan | expectedRevision | the revision of the plan view it writes, or null for a new one |
original.make | expectedRevisions | {"<element id>": <revision>, …}: the revision of each element of elementIds, and of no other |
original.frame | expectedRevision | the group's revision |
original.edit | expectedRevision | the group's revision; each command of the edit gives its own as well |
copy.place | expectedOriginalRevision | the 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:
- One rule, one code. The validator of
@openeik/toolsnames 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 sayopening-fitalike, and a program that handles one handles the other. Numbered codes would have been a second name for each rule. - A code that says what it is. An agent reads
revision-conflictand knows to read again;EIK-CMD-003it 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. - 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:
| Member | Meaning | |
|---|---|---|
code | required | the code (10.1) |
rule | required | the rule it breaks: a Part's number, the section and the rule, as V.3.1 or II.6.1.5 |
pointer | required | a JSON pointer (RFC 6901) into the command, at the member that is wrong: /expectedRevision, /patch/profile; "" for the whole command |
message | required | what is wrong, for people, in the terms of the domain (Part V, 6) |
fix | optional | what would make the command pass, where it is known (10.3) |
data | optional | values for a program: a conflict's expected and current, the records that use what a delete would remove (users) |
record | optional | where in the model the refusal is: {list, id, pointer}, a record and a pointer in it |
- 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.
- 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 anupdatewhose patch gives it or acreateof the record, and at the whole command otherwise, withrecordnaming where the rule is broken. - 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.
- A fix changes nothing by itself. A writer that takes it sends a new command, checked as any is.
- 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. - 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):
| Code | Rule | Refused when | Pointer | Fix | Message (reference) |
|---|---|---|---|---|---|
command-not-object | V.1.1 | a command is no JSON object | "" | ||
kind-missing | V.1.1 | a command names no kind, as a string | /kind | ||
kind-unknown | V.1.2 | its kind is none of the vocabulary's | /kind | ||
kind-not-implemented | V.2 | its kind is of a profile or of the reference vocabulary that the processor does not implement | /kind | ||
command-member-unknown | V.1.4 | it holds a member at its top level that its kind does not define | the member | a move to the member meant, where each is a misspelling | "Unrecognized key(s) in object", with "did you mean" |
command-schema | V.1.2 | it does not match its kind's schema | the first member that does not | the schema's words, by path | |
command-not-json | V.8.2 | it holds what JSON cannot: NaN, an infinity, a BigInt, itself | the first such value | ||
command-too-deep | V.8.2 | it nests deeper than any record may | where it does | ||
patch-undefined | V.4.3 | a patch says undefined for a member its record has | the member | add it as null | "The patch says undefined for …. … Send null to take a field away." |
patch-fixed | V.4.2 | a patch writes an element's id, category or revision | the member | ||
patch-legacy | V.4.6 | a patch writes topRoofId, topCeilingId or roofCutId | the member | ||
patch-removes | V.4.2 | a patch takes away, with null, a member every element has | the member |
The command's own rules (Part V, 3 and 5; stage 2 of Part V, 6):
| Code | Rule | Refused when | Pointer | Fix | Message (reference) |
|---|---|---|---|---|---|
target-missing | V.6.2 | the record the command changes, deletes, copies or names as its target is not there | its id | "Element not found.", "Type not found.", "Level no longer exists." | |
element-of-copy | II.10 | the element it names is a placed copy's, worked out and never stored | its id | ||
revision-conflict | V.3.1 | a revision precondition does not hold | the precondition | read the record | "Revision conflict: expected 3, current 4. Read and retry." |
state-conflict | V.3.3 | a state precondition does not hold | the precondition | read the record | "Project name changed since it was read. Read and retry.", "Level conflict. Read the current level before updating." |
precondition-missing | V.3.2 | a precondition is required and not given: a strict upsert of a record that exists, or any command in an envelope | the member it lacks | read the record | "Grid … exists (revision 2): give expectedRevision 2 to change it." |
precondition-target | V.3.2 | a precondition names a record that is not there: a revision of a new record, a host's revision for an element without a host | the precondition | leave it out, or in an envelope null | |
in-use | V.5.4 | a delete is refused while records use what it would remove: a type, a level and its plans, a view | the target | "Type … is used by 2 elements (…). Reassign them …" | |
level-last | II.3 | it would remove the last level | the target | ||
level-move-to | V.5.4 | a level's delete names in moveTo no other level | /moveTo | ||
group-member | II.10 | it changes an element of a group other than in the group's edit | the element | "… is part of the group G1 (instance G1). Edit the group to change it …" | |
group-by-hand | II.10 | it writes original, copy or born itself | the member | ||
group-twice | II.10 | an element, or one that stands in it, is of another group already | the element | "… is of the group G1 already. An element is in one group." | |
group-not-member | II.10 | it takes out of a group an element not of it | the element | ||
group-hosted | II.10 | it takes out of a group an element that stands in another of the group, without that element | the element | ||
group-instances | II.10 | a group is dissolved while it has other instances | the group | ||
group-origin | II.10 | a group's elements have no place of their own, and no origin is given | /elementIds | ||
edit-nested | II.10 | an original.edit holds another | the inner edit | ||
copies-not-derived | II.10 | it needs a copy's elements worked out, which the processor does not derive | the instance |
The envelope (sections 2 to 8, and 12):
| Code | Rule | Refused when | Pointer |
|---|---|---|---|
envelope-schema | XVII.2 | the envelope does not match its schema, or holds a member it does not define | the member |
envelope-format | XVII.2 | it names a format the processor does not implement | /format |
envelope-required | XVII.8 | the host takes this writer's commands only in an envelope | "" |
operation-reused | XVII.3 | an operation id the processor recorded comes with another envelope | /operationId |
operation-in-progress | XVII.3 | an operation id comes again while its first is being applied | /operationId |
person-mismatch | XVII.4.2 | by.person is not the person the host authenticated, nor one they may act for | /by/person |
target-unknown | XVII.5 | the target's ref is not one the processor keeps, or not one the writer may write | /target/ref |
base-moved | XVII.5 | the ref has moved since the base, and the envelope asks for its base exactly | /target/version |
mode-not-implemented | XVII.6.4 | the mode is one the processor does not implement | /mode |
mode-not-allowed | XVII.6.4 | the host does not let this writer apply to this ref | /mode |
not-permitted | XVII.12 | the writer may not change the target, or a record the commands change | the command, or "" |
too-large | XVII.12 | the 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
- A refusal for a rule the validator checks carries the validator's code for that rule, the code
@openeik/toolsgives the same finding in a folder (itsCODES). 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) andschema-invalid(III.3.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
recordit is on. An error the model had before the batch is no command's: the answer gives it apart (section 11), with its code. - 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). - 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:
| Member | Meaning | |
|---|---|---|
mode | always | the mode the batch was run in |
operation | for 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 |
target | for 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].refusals | for a refused command | its refusals (10.2) |
refusals | where there are any | the refusals that are no one command's: the envelope's (sections 2 to 8), and the errors the model had before the batch |
proposal | for an accepted propose | the proposal file (6.3, rule 1), or the host's proposal: {id, ref, url?} (6.3, rule 2) |
dryRunistruefor the modesdryRunandpropose: nothing was committed to the target.- A refused command keeps its
error, the message of Part V, 6 that names it, beside itsrefusals. - An envelope refused before its commands are tried has no results, and its refusal in
refusals. - 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
- Attribution is personal data.
by.personnames a person, and an operation recorded with itsbysays 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). - 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.
- 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. - 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).
- 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.
- 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.
| Question | Decided |
|---|---|
| 53 | The 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) |
| 54 | A moved base: applied to the ref's newest version, the preconditions deciding, unless the envelope asks for its base exact (5, rule 3) |
| 55 | The agent in the history: a version's commit object gains agents, and a proposal file's by gains agent, in 0.2 (4.3) |
| 56 | A 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) |
| 57 | How 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) |
| 58 | A 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) |
| 59 | Envelopes 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) |
| 60 | An envelope's own members: a member it does not define refused, rather than kept as a record's is (2, rule 2) |