# 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](https://openeik.org/spec/0.3/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.

*Note (informative).* Rule 2 was decided on 11 October 2026. The reference implementation meets it since Stomra pull request #778 (live since 11 October 2026, `main` `8ebedf05`): every version it saves has the leaf `eik.json`, whose object is the canonical JSON of `{format, units}`, and it checks that leaf when it reads a version. A version saved before has none; it is read as `openeik/0.1-draft` in mm (Part I, 9.2, rule 5).

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}}`, and from 0.2 its `agents` (Part XVII, 4.3) and from 0.3 its `app` (section 3.5) where it gives them:
   - `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`.

### 3.5 The application that wrote a version

*Part of 0.3. OpenEik 0.2 had it as the proposal `commitApp` (open questions, 125): a model of 0.2, read by 0.2's rules (Part I, 9.6, rule 3), is read by it only where a caller asks for it (decision D12).*

A version names the application that wrote it, beside the person who made it, its `author`, and the agents whose operations it holds (Part XVII, 4.3). A writer of a version of 0.3 writes it where its caller gives it; one of a version of 0.2 only where the proposal is asked for, without which a version's commit object of 0.2 is rule 1's; and a version of a model of 0.1 never names one.

1. **The member.** A version's commit object MAY carry `app`, `{name, version?}`: the application that wrote the model the version holds, `name` 1 to 100 characters and `version`, where given, 1 to 50, as an agent is named (Part XVII, 4.3), and no other member: `{"commit": {tree, parents, kind, at, author, app?, name?, note?, summary, records}}`. It names one application: of a model two applications' work went into, the one that wrote it as it is.
2. **What its writer says.** It is what the version's writer says, as `author` is: the application that wrote the model, which need not be the one that sends the version to a store, as a command-line tool pushes a version an authoring tool wrote. A reader and a host show it and verify nothing by it, and a host MUST NOT refuse a version because its `app` is not the client that sends it.
3. **In its id.** It is part of the commit object, and so of the version's id (rule 2): a version is never rewritten to add or to change it.
4. **Without it.** A version without `app` names no application, and a reader MUST NOT name one for it, from its `author`, its `note` or the client that sent it. No version written before the proposal names one.
5. **The other applications named.** A result manifest's `app`, `{name, version}` (section 8.2), and a proposal file's `by.app`, a text (section 8.7), keep their forms: this member is a commit object's alone.

Reference: the reference implementation does not write it yet; it asked for it (open questions, 125). In `@openeik/tools`: `commitApp`, and `pushVersion` and `push` with `app` on a folder of 0.3, and with `proposals: ["commitApp"]` too on one of 0.2, in `packages/openeik/src/store.js` and `packages/openeik/src/remote.js`; `openeik push --app <name>[@<version>] --proposal commitApp`.

*Note (informative).* The steward decided on 31 October 2026 that a version names the application that wrote it in 0.3 (open questions, 125): a migration proposal, which moves a line of work to a later version (Part I, 9.8), can then show which applications have written to the line, and so which of them do not write the later version yet. A host that keeps which of its clients sent each version keeps that apart from the version: it is the host's, and says who sent it, not who wrote it.

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

*Note (informative).* Rule 3's `moved` was written on 30 October 2026 with the change of layout (Part III, 4.4; open question 85). Before it, a diff of `@openeik/tools` reported nothing for a record whose path alone changed, so that the paths such a change touches (Part XVI, 4, rule 1) were missed; a host that compares trees leaf by leaf, as EikHub does, found both paths all the same.

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](https://openeik.org/spec/0.3/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](https://openeik.org/spec/0.3/envelope)), and ownership by discipline folder is [Part XVI](https://openeik.org/spec/0.3/ownership)'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:

```text
<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`](https://openeik.org/schemas/0.1/applications/result-manifest.schema.json)):

| Member | Meaning |
|---|---|
| `format` | `"openeik-result/0.1"` |
| `kind` | what the results are, in the application's words: `structural-analysis`, `daylight`, `quantities` … |
| `app` | the application that made them: `{name, version}` |
| `computedFrom` | what they were computed from: `{version, versionKind, inputs, model, upstream}` (below) |
| `engine` | optional: 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 |
| `summary` | optional: the few values a reviewer looks at first, in the application's terms |

The members of `computedFrom`:

| Member | Meaning |
|---|---|
| `version` | the 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) |
| `model` | the 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) |
| `upstream` | `null` 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):

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

```json
{"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`](https://openeik.org/schemas/0.1/applications/proposal.schema.json)):

| Member | Meaning |
|---|---|
| `format` | `"openeik-proposal/0.1"` |
| `intent` | what the change is for, in a sentence a person reads first |
| `base` | what 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 |
| `commands` | the change: one or more commands of Part V, in order |
| `dryRun` | optional: 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 |
| `evidence` | optional: 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):

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