OpenEik.org

Draft OpenEik 0.2.0-draft: draft, not final. From commit 3a7fd749 of 31 October 2026. Errata: the errata of 0.2. Raw text: /spec/0.2/history.md. Earlier: OpenEik 0.1, kept as it was.

On this page

OpenEik 0.2 specification

Part VI. History

A model's history is kept as git keeps code: content-addressed objects, trees that mirror the folder, versions that point at a tree and at the versions before them, and named refs. Two lines of work merge by record and by field, never by text.

The requirements of sections 1 to 5 apply to an implementation that keeps versions of a model (the version store class in Conformance), and those of section 6 when it exchanges versions with another store. A folder alone needs none of them: a folder may be versioned by any tool, git included. Section 8 applies to an application that keeps results or proposals in a model's folder, and to an implementation that reads them.

1. Objects

  1. Every record, every kept file and every tree is an object, named by its hash: the SHA-256 of the UTF-8 bytes of its canonical form (Part III, section 6), as 64 lowercase hexadecimal digits.
  2. A record's object is the record's canonical JSON. A kept file's object is its text as a canonical JSON string.
  3. Two equal records anywhere, in any version, are one object. An object is never changed; a changed record is a new object.

Reference: HashCache.record and buildTree in src/core/versions/tree.ts; sha256 in src/core/versions/sha256.ts.

2. Trees

A tree object is the canonical JSON of {"tree": [[name, kind, hash], …]}:

  • name is one path segment;
  • kind is "t" for a sub-tree and "b" for a leaf (a record or a kept file);
  • the entries are sorted by name, comparing UTF-16 code units.
  1. A model's tree has the folder's layout (Part III), with these differences: a record's leaf is named without .json (architecture/level-0/wall/wall-south), and AGENTS.md and hidden files are not in it. The project record is the leaf project/project, the levels project/levels, the order project/order; a view is drawings/views/<kind>/<id>/view with its chunks under details/. A kept file is a leaf at its own path, with its own name.
  2. eik.json belongs in every stored version's tree, as the leaf eik.json whose object is its canonical JSON, so that a version says which format it is in.
  3. A tree with more than 4096 entries is split into shard sub-trees named ~ followed by the first two hexadecimal digits, lowercase, of the SHA-256 of each entry's name. A shard holds the folder's entries whose names give it, and never a shard: shards are not nested. Shards are not part of any path: a reader of a tree MUST read through them, and MUST NOT treat a ~ name as a folder of the model.
  4. The hash of a model's root tree identifies the model's state: two models with the same root hash hold the same records and kept files.

Reference: buildTree, SHARD_LIMIT, listLeaves and readManifest in src/core/versions/tree.ts. The reference reader reads a folder by building these objects from its files and reading them as a version (manifestOfFolder in src/core/versions/eikFolder.ts): a folder and a version are one format.

3. Versions and refs

  1. A version is a commit object, the canonical JSON of {"commit": {tree, parents, kind, at, author, name?, note?, summary, records}}:
    • tree: the root tree's hash;
    • parents: the hashes of the versions it follows (none for the first, two for a merge);
    • kind: start, auto, named, restore, merge or alternative;
    • at: an ISO 8601 time; author: who made it;
    • name, note: for a named version;
    • summary: a summary of the changes (its form is implementation-defined in 0.1);
    • records: the number of records in the version.
  2. A version's id is its object's hash. Versions are never rewritten: restoring an older state makes a new version on top.
  3. A ref names the newest version of a line of work: main, and alternatives/<slug> for an alternative design. A ref MUST be moved only by compare-and-swap: the writer names the version it expects the ref to point at, and the move fails if it points elsewhere.
  4. An automatic version of a model whose tree has not changed SHOULD NOT be made.

Reference: Commit, VersionHistory.save (the refs main and alternatives/<slug>, and the retry when a ref moved meanwhile) in src/core/versions/history.ts; VersionBackend.setRef in src/core/versions/objects.ts.

4. Diff

  1. A diff between two versions compares their trees. Sub-trees with equal hashes on both sides are equal and SHOULD be skipped without being read.
  2. Changes are reported by record: the list, the id, whether it was added, removed, changed or moved, and the top-level fields that differ (the revision left out). For a view, the counts of detail items added, removed and changed.
  3. A record that moved to another folder (a wall to another level) is one changed record, found by its id, not a removal and an addition, with the path it left and the path it took. A record whose path alone changed, its object the same, is moved: one record, both its paths, no field. The change of layout of Part III, 4.4 moves records so, and so does a file renamed to the form of Part III, 2, rule 9.

Reference: treeDelta and diffTrees in src/core/versions/diff.ts.

5. Merge

A merge joins two lines of work, ours and theirs, against the version both started from, the base.

  1. Records are matched by id, list by list, never by path or by text.
  2. For each record:
    • changed on one side only: that side's record is taken;
    • added on one side only: it is taken;
    • deleted on one side and unchanged on the other: it is deleted;
    • changed on both sides: it is merged member by member (rule 3);
    • changed on one side and deleted on the other: a conflict (changed and deleted);
    • added on both sides with different content: a conflict (added twice).
  3. A member changed on one side only takes that side's value. Objects merge member by member, recursively. A list whose items are all objects with an id (a type's layers, a view's detail items, a sheet's viewports) merges item by item by id: our order, with their new items placed after the item that precedes them on their side. Any other value (a number, a point, a list without ids) changes as a whole. A member changed differently on both sides is a conflict (both changed), reported by its path.
  4. A record merged from both sides gets the revision max(ours, theirs) + 1.
  5. The project record and the project information merge as one record each; the levels merge as a list by id, and a conflict in them is settled for the whole list; kept files merge file by file. A kept file of one side at a path that the other side's records or kept files need, as a file or as a folder (Part III, 7.4), or whose path folds to one with a path of the other side's (Part III, 4.3, and 7, rule 8), is a conflict (in the way), reported with both sides' paths: the merged model could not be written as a folder otherwise. Of two kept files whose paths fold to one, the conflict is on the later in the order of their paths by UTF-16 code units, which a reader would leave alone.
  6. A conflicted record keeps our side until a person chooses ours or theirs for it. A record settled by a choice, which both sides hold, keeps the chosen side's revision when that revision is the higher of the two, and otherwise gets max(ours, theirs) + 1 (rule 4): either way, no earlier read of either side's record names the settled one, so a command based on it is refused (Part V, 3).
  7. The merged model MUST pass the whole-model check (Parts I and II) before it is accepted. A merge that leaves a host missing or a type deleted under its elements MUST be refused, naming the records, never applied silently.

Reference: mergeManifests, mergeObject, mergeKeyed and resolveMerge in src/core/versions/merge.ts; the store's restore checks the merged model whole.

6. Remotes, push and pull

A model's versions may be kept in more than one store: an application's own store and a repository on a host, two hosts, or a folder and a host. This section says how two stores of one model exchange versions, as git's remotes do: work stays local until it is pushed, and what others pushed is pulled and merged.

It says what a fetch, a push, a pull and a clone do, not how objects and refs travel between two stores: OpenEik 0.1 defines no protocol between stores (Security and privacy, 1). The store API that @openeik/tools speaks is one store's. The steward deferred such a protocol to a later version on 15 October 2026 (open questions, 28).

6.1 Remotes and tracking refs

  1. A remote is another store that holds a repository of the same model. The local side gives it a name (origin).
  2. A remote-tracking ref records, for one remote and one of its refs, the version last fetched from it or pushed to it: {remote, ref, version, tree}, where tree is that version's root tree. A tracking ref MUST record the version and the tree. An implementation MAY add members: when, by whom, the repository.
  3. Tracking refs are kept apart from the local refs (section 3): they are never written to the shared repository, and no version's tree holds them. Where an implementation keeps them is its own choice, except in a folder.
  4. A folder keeps its tracking refs in the hidden file .eik/remotes.json, {"<remote>": {"url": …, "repo": …, "refs": {"<ref>": {"version": …, "tree": …}}}}, which is not part of the model or of any version's tree (Part III, 1, rule 1). A reader of .eik/remotes.json MUST keep the members it does not know (Part I, 10).

6.2 Fetch

A fetch of a remote ref reads every object reachable from the remote ref's version that the local store lacks (versions, trees, records and kept files: section 1), and then sets the tracking ref to that version and its tree. A fetch that is part of a pull sets the tracking ref only once the pull's result is recorded (6.4), so that a pull that stops at a conflict leaves the tracking ref, and with it the next pull's base, where it was. A fetch moves no local ref.

6.3 Push

A push sends a local line of work to a ref of a remote.

  1. By compare-and-swap from the tracked version. It moves the remote ref only by compare-and-swap (section 3, rule 3), from the version the tracking ref records (from none, for a ref the remote does not have yet). If the remote ref has moved since, nothing is written on the remote: the pusher pulls first (6.4), and pushes again.
  2. A published version is never rewritten. The pushed version MUST descend from the tracked remote version: that version is in its history, through its parents. So the remote ref only moves forward, and no version the remote holds is rewritten or dropped. A squashed version (rule 3) has the tracked remote version as its first and only parent.
  3. Automatic versions stay local. A version of kind auto MUST NOT be pushed to a shared ref. A tool MAY squash its local versions that are not on the remote into one version of kind named, with the push's message as its name, whose tree is the newest local tree and whose only parent is the tracked remote version: no other kind of version is needed. It MUST NOT squash a version that is on the remote already.
  4. The pushed version's summary covers every change since the tracked remote version.
  5. Only the objects the remote lacks are sent.
  6. A push MUST NOT drop the members of records that the pusher does not know, nor kept files it did not change (Part I, 10; Part III, 7): the pushed tree holds them as the local one does.

6.4 Pull

  1. A pull fetches the remote ref (6.2), then merges the remote version into the local version (section 5). The base is the version the tracking ref recorded before this pull: the remote version the local line last took in or sent out. Only where none is tracked is the base the newest version both lines have, found from their parents. The tracked version, not a version found from the parents, is the base because a line that squashed its pushes (6.3, rule 3) does not hold the version it pushed, and an ancestor found from the parents would count that push's changes twice. The pull checks the result whole (section 5, rule 7) and records a merge version whose parents are the local version and the remote version, in that order. Conflicts are settled as in any merge (section 5, rule 6).
  2. When the local line holds the tracked remote version itself and has no version of its own after it, a pull MAY move the local ref to the remote version (a fast-forward) instead of recording a merge version. A line that squashed its pushes does not hold the remote version, so its pull records a merge version even when nothing local changed.
  3. A folder that keeps no history of its own pulls by merging the remote version into its current files, with the tracked version as the base, writing the result as a writer does (Part III, 13), and then setting its tracking ref to the remote version. When its files still hold the tracked version's tree, it writes the remote version as it is (a fast-forward).
  4. Kept files merge file by file (section 5, rule 5). A kept file that the remote line removed, and the local line did not change, is removed by the pull: the removal was made by the application that owns the file (Part III, 7.3), and keeping it would bring it back with the next push. Files a reader leaves alone (Part III, 7, rules 2 and 8) are never touched.

6.5 Clone

A clone fetches the refs asked for from a remote, main when none is named (6.2), sets each local ref to its version, and records a tracking ref for each ref it fetched and for no other (6.1). A clone into a folder fetches one ref, since a folder holds one version (Part III). A store or an application that keeps several refs MAY clone every ref of the remote.

Reference: clone in this repository's packages/openeik/src/remote.js clones one ref into a folder, main by default. Stomra 500 (live since 14 October 2026, main cd7173c9) clones main, a single-ref clone, into a new project connected to the repository as its remote.

6.6 Which versions are shared

Versions of kind start, named, merge, restore and alternative belong in a shared history. Versions of kind auto stay local (6.3, rule 3).

Reference: Stomra 495 (live since 14 October 2026, main a7b7b13b) pushes and pulls one remote ref, and keeps its tracking ref in the project's settings, with the repository, the version and the tree, and when and by whom it was set. It does not clone yet.

7. Git and other hosts (informative)

A folder is plain files, so it can be kept in git or any other version control system and in any object store, and pushed, pulled and reviewed there. Git's push and pull move git's commits, by git's rules; section 6 is the push and pull of OpenEik's own versions. A text merge of two stored-form files is often correct, because each member is one line, but it does not know records: it can join two edits that are each valid into an invalid model, and it cannot merge a moved record. An implementation SHOULD merge with the rules of section 5 and check the result whole. No git merge driver is specified in 0.1.

Forks that remember their origin are planned and not specified. A host's proposals (changes offered for review, as pull requests are) come from an envelope in 0.2 (Part XVII, 6.3), and ownership by discipline folder is Part XVI's. Where no host offers proposals, an application offers its changes in a proposal file (section 8.7).

8. Applications' results and proposals

An application that works beside a model's author (a structural analysis, a daylight study, a quantity take-off, a supplier's tool) keeps its own work in the model's folder as kept files (Part III, 7): its own model of the building, the results it computes and the changes it proposes. It never writes them into the model's records. This section fixes where they go, the result manifest, which says what a set of results was computed from, and the proposal file, which offers changes to the records as commands. Kept with the model, they travel with it through every copy, version, restore and merge (sections 2 and 5).

8.1 The application's folder

An application keeps its files in a folder of its own at the top of the model's folder, with one model folder for each model of its own that it keeps there:

<application>/<model>/                                  the model folder: one model of the application's
<application>/<model>/source.json                       what the model was built from (8.4)
<application>/<model>/…                                 the application's intent files: its model, settings, assumptions …
<application>/<model>/results/<run>/manifest.json       a result set's manifest (8.2)
<application>/<model>/results/<run>/…                   the result set's files
<application>/<model>/proposals/<id>.json               a proposal (8.7)

For example, an analysis whose prefix is acme keeps acme/frame-a/model.json, acme/frame-a/source.json, acme/frame-a/results/r-5d1c0e9a7b32/manifest.json with its results beside it, and acme/frame-a/proposals/p-header-deeper.json.

  1. An application MUST keep its result sets and its proposals in this layout: each result set in a run folder results/<run>/ of a model folder, with its manifest at results/<run>/manifest.json, and each proposal at proposals/<id>.json of a model folder. <application> is the application's folder, which Part I, 11.3 says to name by its prefix (acme); <model>, <run> and <id> are its own ids as path segments (Part III, 2). As kept files, these files MUST NOT take a record's path or a folder a record needs (Part III, 7.4): an application's folder is never a discipline folder or a record list's folder.
  2. The intent files of a model folder are its kept files outside its results/ and proposals/ folders: what the application keeps of its own, source.json included. OpenEik does not define them, beyond source.json (8.4).
  3. Every file of an application's folder is a kept file only if it is UTF-8 text of at most 1 MiB at a path a kept file may have (Part III, 7, rules 1 and 8); any other file is left alone and is not part of the model (Part III, 7.2). An application SHOULD write its JSON files in the stored form (Part III, 5), one item per file, so that a change is a small diff and a merge goes file by file (section 5, rule 5).
  4. Results and intent files state their own units (Part III, 7.6).

8.2 The result manifest

A result set is the files of one run folder: its manifest and the results themselves, in a form the application chooses. The manifest is a JSON object (schema applications/result-manifest.schema.json):

MemberMeaning
format"openeik-result/0.1"
kindwhat the results are, in the application's words: structural-analysis, daylight, quantities …
appthe application that made them: {name, version}
computedFromwhat they were computed from: {version, versionKind, inputs, model, upstream} (below)
engineoptional: what else decides the results, in the application's terms (a kernel, a code pack and its edition)
made{at, by}: when they were made, as an ISO 8601 date and time with seconds and an offset (2026-10-11T18:20:00Z), and by whom: the id or email of the person who made them, not only a display name. A run that no person started, as an application or an agent makes one unattended, names that application or agent instead, by an id of its own
summaryoptional: the few values a reviewer looks at first, in the application's terms

The members of computedFrom:

MemberMeaning
versionthe version of the model that was read: a version's id (section 3; versionKind "openeik"), a git commit's id ("git"), or null for a folder that is under no version control. It is the version before the results were saved: the version that saves them is a child of it
versionKind"openeik" or "git"; null exactly when version is null
inputs{count, digest}: how many records the application read, and their digest (8.3)
modelthe digest of the model folder's intent files (8.3). Its source.json is one of them (8.4, rule 1), so a writer always has a digest to write. null says that the folder had no intent files, and so no source.json: a reader reads it, and never calls those results current (8.4, rule 3)
upstreamnull on a model's main line. On a fork or an alternative (a simplified model on a line of its own): {repository, ref, version}, where the line started: the repository (a URL or owner/name; null for the same repository), the ref it started from (main) and that ref's version when it did
  1. A writer MUST write format as openeik-result/0.1. A reader MUST read openeik-result/0.1-proposal as the same format: manifests written before 14 October 2026 carry that id. A manifest of any other format is not one this version defines, and a reader MUST NOT say whether its results are current.
  2. format, kind, app, made and computedFrom, with all five of its members, are required. engine and summary are the application's; OpenEik does not define their members. A reader ignores members it does not know. An application's own data goes in engine and summary, and in the other objects of the manifest only as extension members (Part I, 11.2, rules 1 and 8).
  3. A result set on a fork or an alternative names a version of that line in computedFrom.version, and the version of the line it started from in upstream. When the line is merged back, its result sets come along, and their upstream still says on which line they were computed.

Example (engine, made and summary as an analysis might give them; the hashes are illustrative):

{
  "app": {"name": "an-analysis-app", "version": "0.5.0"},
  "computedFrom": {
    "inputs": {"count": 41, "digest": "3b0f6c2e9d41a8f07e2c5b19d6a4f0c8e7b3a2d1c9f8e7d6c5b4a3f2e1d0c9b8"},
    "model": "a41e0c7d5b3f2e1d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d",
    "upstream": null,
    "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d",
    "versionKind": "openeik"
  },
  "engine": {"kernel": "an-analysis-app 0.5.0"},
  "format": "openeik-result/0.1",
  "kind": "structural-analysis",
  "made": {"at": "2026-10-11T18:20:00Z", "by": "jane@example.com"},
  "summary": {"maxUtilisation": 1.08}
}

8.3 Digests

A digest names a set of files by their content, so that it is the same on a disk, in git and in any store:

  1. Each file gives one pair [path, hash]: its leaf's path in a version's tree (section 2) and the hash of its object (section 1). For a record, that is its path without .json (architecture/level-0/wall/wall-south, project/levels) and the SHA-256 of its canonical JSON (Part III, 6); a view's pair is its view.json's, without its detail chunks. For a kept file, it is its own path (acme/frame-a/model.json) and the SHA-256 of its text as a canonical JSON string.
  2. The pairs are sorted by path, comparing UTF-16 code units, each path once.
  3. The digest is the SHA-256, as 64 lowercase hexadecimal digits, of the canonical JSON of the sorted list: an array of two-string arrays.
  • computedFrom.inputs.digest is the digest of the records the application read, and inputs.count the number of their pairs. project/order.json is never an input.
  • computedFrom.model is the digest of the model folder's intent files (8.1, rule 2), source.json among them. A model folder without intent files, which a writer that meets 8.4, rule 1 never leaves, has model null, not the digest of an empty list.

For example, the south wall and the levels of examples/one-storey-house give the pairs [["architecture/level-0/wall/wall-south","fe517b8853f582a78162d5e56ca5353b943c01b18caf6858099404d0c42b411c"],["project/levels","29da0824e3f9d2392427e4ffa02ccdd7a2f41bad6fb1bfdcb19d7ad35dfcd948"]], which is also their canonical JSON, and the digest 075c94e68d1ab7fbbde6248eebadffb2d558044cebbf39219c3915334e64a23a (count 2).

Reference: recordInputs, inputsDigest, intentInputs, runId and resultStatus in this repository's packages/openeik/src/result.js compute the pairs, the digests, the run id (8.5) and the test of 8.4.

8.4 When results are current

  1. The selection. Which records an application reads is its selection. An application that writes results MUST keep it in the model folder's source.json, a JSON object with at least:

    • select: {categories?, lists?, ids?, paths?}, each a list of strings: the elements whose category is in categories; every record of each list in lists, named as the single-file form names it (Part III, 11), with project for the project record and levels for the levels; the records whose id is in ids, in any list but the project, the levels and the order; and the records whose leaf path is in paths, a path naming its record in either layout of Part III, 4.4. A record that any of them names is selected.
    • inputs: the pairs (8.3) of the records the selection selected when the application last read the model: the list behind computedFrom.inputs.

    Its other members are the application's. source.json is an intent file, so a change to the selection, or a new reading of the model, changes computedFrom.model.

  2. Current or stale. A result set is current in a version or a folder when both hold there:

    • the records its application's selection selects give computedFrom.inputs.digest;
    • the model folder's intent files give computedFrom.model.

    Otherwise it is stale. Comparing computedFrom.version with the newest version is not enough: the version that saves the results is a child of the one they were computed from, and later versions that change no input leave them current.

  3. A reader that says whether a result set is current MUST decide it by rule 2, and rule 5. Without a select in source.json it cannot: it MUST NOT call the results current.

  4. Where a result set is stale, comparing source.json's inputs with the pairs of the records at those paths now says which inputs changed, were added or went. A pair is compared with the record its path names, by its list and id in either layout of Part III, 4.4 (its rule 9), so that a record that only moved is the same input.

  5. Records that only moved. Where the digests differ, a result set is current all the same when the pairs of source.json's inputs give computedFrom.inputs.digest and name, record by record (rule 4), exactly the records the selection selects now, each with the hash its pair gives: the records moved, as the change of layout moves them (Part III, 4.4, rule 8), and none changed. A reader MUST NOT call such a result set stale for that reason alone.

8.5 The run id

A run folder's name is its run id. It SHOULD be r- followed by the first 12 hexadecimal digits of the SHA-256 of the canonical JSON of {"engine": …, "inputs": …, "model": …}, whose members are the manifest's: its engine (left out when it has none), computedFrom.inputs, the object {count, digest} as it stands, and computedFrom.model, a digest or null. The manifest of the example in 8.2 gives

{"engine":{"kernel":"an-analysis-app 0.5.0"},"inputs":{"count":41,"digest":"3b0f6c2e9d41a8f07e2c5b19d6a4f0c8e7b3a2d1c9f8e7d6c5b4a3f2e1d0c9b8"},"model":"a41e0c7d5b3f2e1d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d"}

and so the run id r-2e13b39cbc7a. The same inputs on the same engine give the same run id: a run made again goes to the same run folder. Where the application is deterministic, it gives the same results, and a manifest that differs from the first only in made, and in computedFrom.version where it read a later version that changed none of its inputs.

  1. An application that makes a run again into a run folder that exists SHOULD leave the folder as it is when the run gives the same results and a manifest that differs only in made and computedFrom.version: the first manifest stays, with its made and its version, which still say truly when, by whom and from what the results were computed, and the version does not change. Where anything else differs, it replaces the folder's files, the manifest among them.

8.6 Old results

An application that writes results into a folder SHOULD keep there the latest result set of each model and every set that is named or signed, and remove the others: the history keeps them. Removing a run folder is an ordinary change.

8.7 The proposal file

Until a host offers proposals (section 7), an application offers changes to a model's records in a proposal file, <application>/<model>/proposals/<id>.json, never by writing the records itself. It is a JSON object (schema applications/proposal.schema.json):

MemberMeaning
format"openeik-proposal/0.1"
intentwhat the change is for, in a sentence a person reads first
basewhat it was made against: {version, records}. version names a version as computedFrom.version does (8.2), or is null; records gives, by id, each record that the commands change as it was there: {hash, revision}, its hash (Part III, 6) and its revision (Part I, 8)
by{app, person}: the application that made it, with its version ("an-analysis-app 0.5.0"), and the person it was made for: an id or an email
commandsthe change: one or more commands of Part V, in order
dryRunoptional: the last dry run of commands (Part V, 7.3), {at, by, ok}: when, by which command processor and version, and whether it passed; or null when none was made
evidenceoptional: why, in the application's terms: a check, the values before and after, the result set that found it by its manifest's path
  1. A writer MUST write format as openeik-proposal/0.1. A reader MUST read openeik-proposal/0.1-proposal as the same format: proposals written before 14 October 2026 carry that id. An implementation MUST NOT apply a proposal whose format it does not implement.
  2. format, intent, base, by and commands are required. A reader ignores members it does not know. An application's own data goes in evidence, and in the other objects of the proposal only as extension members (Part I, 11.2, rules 1 and 8).
  3. commands is a batch (Part V, 7): applied in order, all or nothing, and checked as any batch is (Part V, 6). Each command that changes a record SHOULD carry its kind's precondition (expectedRevision …) matching base, so that a command made against a record that has changed since is refused rather than applied over the change.
  4. A person accepts a proposal. Writing a proposal changes no record. The person, or a command processor or host acting on the person's word, runs commands as one batch on the line of work the person chooses, after a dry run if they like; or the person merges a line of work (an alternative, a branch) on which the commands were applied (section 5). A refused batch changes nothing, and the proposal stays as it was.
  5. An application MUST NOT apply its own proposal to a line of work that a person edits, unless that person accepts it. It MAY apply it on a line of its own, made for the proposal, for the person to review and merge.
  6. dryRun and evidence inform the person; they decide nothing. The batch run on acceptance decides.
  7. A proposal file MUST NOT carry costs, prices or quotes: it is part of the model, and they are not (Part II, 12).

Example, a deeper beam proposed after an analysis (the hashes are illustrative):

{
  "base": {
    "records": {
      "outdoor-header-0": {"hash": "a825935f50e8ecccaeb45f691fde75f186dea325a1cd8c1490399d48ae9174e3", "revision": 0}
    },
    "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d"
  },
  "by": {"app": "an-analysis-app 0.5.0", "person": "jane@example.com"},
  "commands": [
    {"expectedRevision": 0, "id": "outdoor-header-0", "kind": "update", "patch": {"height": 270, "typeId": "type-beam-glulam-90x270"}}
  ],
  "dryRun": {"at": "2026-10-11T18:30:00Z", "by": "a command processor 1.0", "ok": true},
  "evidence": {"after": 0.81, "before": 1.08, "run": "acme/frame-a/results/r-5d1c0e9a7b32/manifest.json"},
  "format": "openeik-proposal/0.1",
  "intent": "Deeper header: bending utilisation 1.08"
}