# @openeik/tools

Read, check, hash, diff and store OpenEik building models, and print them as IFC, from the [specification](../../spec/README.md) alone. A library for apps (Node.js and browsers) and the `openeik` command for people, scripts and AI agents.

| | |
|---|---|
| Version | 0.4.0: reads and writes OpenEik 0.1, 0.2, 0.3 and 0.4 (`openeik/0.1-draft`, `openeik/0.1`, `openeik/0.2`, `openeik/0.3`, `openeik/0.4`, each also with a pre-release suffix); a model is written in its own version, one that names none as 0.1 unless asked for a later one, which migrates it ([Versions](#versions)) |
| Status | Draft. Not yet published to npm |
| Conformance | Claims the writer and compiler (core) classes of OpenEik 0.2.0-draft, and of OpenEik 0.1. Implements the reader, validator and command processor classes, but does not claim them: its checks leave out a rule of Part II and check another in part. Implements Part VI's versions, diff, merge and remotes as a client of a store, the IFC bridge (Part VIII, 1) and the drawing validator (Part XIV, 19.2; Part XVIII, 9), which it does not claim either. See [Conformance](#conformance) |
| Runs on | Node.js 20.16 or later; any modern browser or worker for everything that does not take a folder's path |
| Dependencies | None |
| Licence | [Apache-2.0](LICENSE). The bundled copy of `schemas/` (`src/schemas.js`) is [CC0-1.0](../../schemas/LICENSE), as the schemas are. The Unicode 13.0.0 data of `src/unicode.js`, derived from the Unicode Character Database, is under the Unicode License v3, and IFC4's property set templates in `src/ifc4-psets.js`, derived from buildingSMART's ifc-gherkin-rules, are under the MIT License ([`NOTICE`](NOTICE)) |

It is written from the specification and its schemas, not from any implementation. Its version trees have the same root hash as the reference implementation's for the same folder (see [Hashes](#hashes-and-the-reference-implementation)).

## Quick start for an app

From this repository (later: `npm install @openeik/tools`):

```js
import { readFolder, checkFolder, versionTree, connectStore, pushVersion, readVersion } from "@openeik/tools";

// Read a folder. It is checked whole and refused, with the file and the rule, if it fails.
const house = readFolder("examples/one-storey-house");
house.model.elements;                      // the single-file form: elements, types, views, levels …
house.get("elements", "wall-south");       // a record by list and id
house.pathOf("elements", "wall-south");    // "architecture/level-0/wall/wall-south.json"
house.kept;                                // files that are no record (acme/, README.md …), text by path

// Check it, and read the diagnostics.
const { ok, errors, warnings } = checkFolder("examples/one-storey-house");
// each: {severity, code, path, pointer?, message, rule}, e.g. code "ref-missing", rule "I.7"

// Hash it: the root of its version tree.
const tree = versionTree("examples/one-storey-house");
tree.root;                                 // 64 hex digits; the same folder gives the same root anywhere

// Push it as a version to a store that speaks EikHub's API v0, and read it back.
const store = connectStore({ url: "https://api.eikhub.com", repo: "acme/house", token: process.env.OPENEIK_TOKEN });
const { version } = await pushVersion(store, "examples/one-storey-house", { author: "Jane Doe", name: "Ground floor" });
const back = await readVersion(store, version);   // or readVersion(store, "main")
back.model.elements.length;
```

In a browser, pass the files instead of a path: a `Map` (or an object) of relative paths to texts or bytes, for example from a zip or a file picker:

```js
const files = new Map([["eik.json", "…"], ["project/project.json", "…"], /* … */]);
const house = readFolder(files);
```

### Writing a folder

`writeFolder` writes a model in the single-file form onto a folder, as a writer must (spec, Part III, 13). It checks the model first and refuses it, with the diagnostics, when it has errors; writes only the files whose bytes change; removes the files of records that are no longer in the model and no other file (kept files, hidden files, `AGENTS.md` and files the reader leaves alone stay); and writes `AGENTS.md` only where there is none, under any case, and the model keeps no file of that name in another case (`agents.md`), which it would write over where case is ignored.

A record's path is made of its id, level, category and kind (Part III, 2 and 4.2), and never leads out of the folder, on this system or any other. An id or a level is written as its path segment (Part III, 2): `%` and the characters that common file systems refuse (`/ \ : * ? " < > |` and control characters) as `%` and two upper-case hexadecimal digits, and so a leading `.` or `~`, a trailing `.` or space, and the first letter of a device name with or without an extension (`con`, `Aux.1`, `lpt¹`). So `study-roof:gutter:study-roof:edge:0` is in `study-roof%3Agutter%3Astudy-roof%3Aedge%3A0.json`, `.x` in `%2Ex.json`, and `x\..\..\evil` in `x%5C..%5C..%5Cevil.json`: no id is refused, an empty one aside, and every name is one that Windows, macOS and Linux take and read as it is written. The reader decodes every `%` and two hexadecimal digits, so a folder written before 14 October 2026, with a `:` as it is in a name, reads as the same ids, with a warning naming the path the rules give (`path-unescaped`); writing it moves each such file to its path. A category and a view's kind are not escaped: one that would lead out of the folder where `\` separates folders as on Windows (a category of a later version `a\..\..\..\..\x`), that Windows would read into a hidden folder or file (`.claude`, `.vscode`, `.git`, `.eik`), a reserved name or `eik.json`, or that makes a path segment `.` or `..`, which no file system holds as a name, is refused (`model-invalid`, with a `path-unsafe` diagnostic naming the record), as is one that would make a hidden, reserved or empty name or holds `/` or NUL; one with a `\` between plain names is written as it is. `writeFolder` checks again that every path it touches stays in the folder before anything is written. A write is all or nothing: every path is checked first (two records or two levels' folders whose paths fold to one, at any depth, are refused as `path-collision`, and a kept file whose path folds to one with another of the model's paths, or holds a name a file system refuses or reads otherwise, as `kept-fold` or `kept-name`, since case-insensitive file systems would make them one file and some refuse or read the names otherwise, Part I, 6, rule 7, and Part III, 7, rule 8; the validator compares a model's paths in the same way, `path-fold` an error, and `kept-fold` and `kept-name` warnings of a folder, whose reader leaves such a file alone, and errors of the single-file form, which holds it; an id or a level whose segment is over 200 bytes is refused, Part I, 6, rule 8, as is a name over 255 bytes, and a path over the system's limit as `path-too-long`), the new files are staged in the hidden `.eik/` folder and moved into place, a folder or a file renamed only in case is renamed, and the files of records that went are removed last. A failure on the way is a `write-failed` naming the path, and leaves the folder as it was or as a later write completes it. Nothing is written through a link: a hidden `.eik` that is a link or no folder is refused before anything is staged (`eik-unsafe`), and so is any path the write touches that goes through a link (`path-unsafe`), whatever it points at. A file the reader leaves alone (binary, or over 1 MiB) is never overwritten, and a folder whose `eik.json` names no format this package implements is not written into.

```js
import { readFolder, writeFolder, modelFiles } from "@openeik/tools";

const { model } = readFolder("./house");
const wall = model.elements.find((e) => e.id === "wall-south");
wall.height = 3000;
wall.revision += 1;                        // a record changed by hand: its revision goes up by 1
writeFolder("./house", model);             // {written: ["architecture/level-0/wall/wall-south.json"], removed: [], unchanged: 32}

modelFiles(model);                         // the same files in memory, path to text: anywhere, a browser too
```

A folder read and written back unchanged is what it was, byte for byte (`AGENTS.md` aside), so its version tree has the same root. Each record goes where Part III, 4 puts it; a `component` goes in `structure` when it is `structural`, a plate or a cast-in item (this package has no library of connectors); a `generic` one in the folder its `generic.discipline` names (an element's own, else its type's), else in `architecture` (rule 5). An element or a type of a category this package does not know, one a later version adds, is written back unchanged in the discipline folder it was read from (rule 6): `writeFolder` finds its file in the folder by list and id (through folded names, so an id changed only in case still finds it), `modelFiles(model, {from})` takes the paths of the folder a model was read from, and with none (the single-file form) it goes under `architecture`. A view's detail items are written in chunks by the rule of Part III, 10. From the command line: `openeik write <model.json> <folder>` and `openeik read <folder>`. The pieces are in `@openeik/tools/write` too: `detailChunks`, `chunkNames`, `idHash`, `elementDiscipline`, `typeDiscipline`; and `modelPaths(model)` gives the paths `modelFiles` writes a model at, which the validator compares as the writer does.

### Saving results beside a building

An app keeps its own files in a top-level folder of its own, named by its registered prefix (`acme/<model>/` for an app registered as `acme`), and names what each result set was computed from in a result manifest, `openeik-result/0.1` ([spec, Part VI, 8](../../spec/06-history.md#8-applications-results-and-proposals)):

```js
import { recordInputs, inputsDigest, intentInputs, runId, resultManifest, resultStatus } from "@openeik/tools";

const select = { categories: ["beam", "column", "slab", "wall"], lists: ["levels", "types"] };
const inputs = inputsDigest(recordInputs(house, select));            // {count, digest}
const model = inputsDigest(intentInputs(house, "acme/frame-a")).digest;
const engine = { kernel: "an-analysis-app 0.5.0" };
const manifest = resultManifest({
  app: { name: "an-analysis-app", version: "0.5.0" }, kind: "structural-analysis",
  version, inputs, model, engine, made: { at: new Date().toISOString(), by: "jane@example.com" },
});
const id = runId({ inputs, model, engine });                         // "r-" and 12 hex digits

// Later: are the results still current?
resultStatus(manifest, { inputs: inputsDigest(recordInputs(readFolder(dir), select)), model });
// {current, inputs, model}
// After a building's change of layout (Part III, 4.4) the digest is another; given the pairs source.json kept and
// today's, a result set whose records only moved is current (Part VI, 8.4, rule 5).
resultStatus(manifest, { inputs, model, pairs: recordInputs(readFolder(dir, { proposals: ["ownership"] }), select), kept: source.inputs });
```

### A repository that belongs to an application's project

When a store says that a repository belongs to the project of an authoring application (its `source` is set), the main line is that application's. `pushVersion` refuses to write `main` there unless `allowSourceMain` is set (`--allow-source-main`), and an app writes an alternative instead, starting from the main line's version:

```js
await pushVersion(store, folder, { author: "An analysis app for Jane Doe", ref: "alternatives/frame-a-results", parent: mainVersion });
```

An authoring application may keep its own list of the alternatives it shows; this package does not write it. `eikJson: false` leaves `eik.json` out of the tree, as versions saved before 11 October 2026 were built (Part I, 9.2, rule 5); a version of 0.1 holds it (Part VI, 2.2). [Reading and saving an OpenEik model](../../guides/reading-and-saving.md#103-a-repository-that-belongs-to-an-authoring-application) has the guide's advice.

## Quick start for an AI agent

Work on a folder as on a code base; check after every change.

```sh
openeik check ./house                   # 0: valid; 1: errors, each with its file, rule and code
openeik check ./house --json            # the same as JSON: {ok, files, counts, diagnostics: [{severity, code, path, pointer, message, rule}]}
openeik check ./house --quality         # also: elements modelled under a category that does not fit (warnings)
openeik tree ./house architecture --leaves   # the layout, with a hash and a count per folder
openeik diff ./before ./after           # what changed, by record and field; exit 1 when anything did
openeik hash ./house                    # the root hash: equal hashes, equal models
```

The loop:

1. Read `AGENTS.md` in the folder. Map with `openeik tree` or `ls`, and read only the records you will change.
2. Change a record's file: keep every member you do not know, add 1 to its `revision`, keep the stored form (sorted keys, two spaces, a member a line). Never write derived data (bodies, areas, joins worked out from geometry).
3. `openeik check <folder>`. Done means no errors. Fix what it names: each diagnostic has a `code` and the `rule` of the specification it comes from. The codes are this library's own, and may be renamed when the specification defines codes, planned for 0.2.
4. `openeik diff` against the folder as it was, and say what changed by record id.

With a store, the folder is a working copy ([Remotes](#remotes-clone-push-pull)): `openeik clone` it once, work in the loop, then `openeik push <folder> -m <message>`; when the push says the ref moved, `openeik pull <folder>` and push again.

A store's token is never an argument: put it in `OPENEIK_TOKEN` (or the variable `--token-env` names), and the origin of the store it is for beside it, in `OPENEIK_TOKEN_ORIGIN` (`<NAME>_ORIGIN`). The command refuses `--token`. It sends the token only over https (or plain http to this computer), to a store named with `--url`, or to a folder's remote only when the remote is at that origin: a folder's `.eik/remotes.json` travels with the folder and may come from anyone ([Tokens](#tokens)).

## The command

```text
openeik check <folder | model.json> ...   [--json] [--quiet] [--verbose] [--quality] [--schemas <dir>] [--proposal electrical | drawings | ownership | assets | review | grain | site | classifications | railings | declared | components | adopted]
openeik compile <folder | model.json>     [--json] [--quantities] [--verbose] [--proposal electrical | declared | framing]
openeik ifc <folder | model.json>         [--out <file.ifc>] [--version <root>] [--version-id <id>] [--at <time>]
                                          [--printed-by <name>] [--crs <EPSG:code> [--vertical-datum <name>]]
                                          [--bodies <file.json>] [--json] [--proposal electrical]
openeik ifcx <folder | model.json>        [--out <file.ifcx>] [--version <root>] [--version-id <id>] [--at <time>]
                                          [--printed-by <name>] [--bodies <file.json>] [--json] [--proposal electrical]
                                          (EXPERIMENTAL: buildingSMART's ifcx_alpha)
openeik hash <folder>                     [--no-eik-json] [--json] [--proposal electrical]
openeik tree <folder> [<path>]            [--leaves] [--depth <n>] [--no-eik-json] [--proposal electrical]
openeik diff <folderA> <folderB>          [--json] [--no-eik-json] [--proposal electrical]
openeik write <model.json> <folder>       [--no-check] [--json] [--verbose] [--proposal electrical]
openeik read <folder>                     [--out <model.json>] [--proposal electrical]
openeik datums <folder> <out>             [--json]
openeik clash <folder | model.json>       [--tolerance <mm>] [--json] [--proposal electrical]
openeik merge <base> <ours> <theirs>      [--json] [--verbose] [--out <file.json>]
openeik clone <folder> --url <base> --repo <owner/name> [--ref main] [--remote origin] [--json] [--proposal assets | electrical]
openeik push <folder> -m <message>        [--author <name>] [--note <text>] [--remote origin] [--ref <ref>] [--json] [--proposal assets | electrical]
                                          (a folder with .eik/remotes.json; the author also from OPENEIK_AUTHOR)
openeik pull <folder>                     [--choose <key>=ours|theirs ...] [--remote origin] [--ref <ref>] [--json] [--proposal assets | electrical]
                                          (a folder with .eik/remotes.json)
openeik status <folder>                   [--fetch] [--json] [--proposal assets | electrical]
openeik push <folder> --url <base> --repo <owner/name> --author <name>
                      [--name <text>] [--note <text>] [--ref main | alternatives/<slug>] [--parent <hash>]
                      [--no-eik-json] [--allow-source-main] [--no-check]      (a folder without one)
openeik pull <folder> --url <base> --repo <owner/name> [--ref <name> | --version <hash>] [--force]
openeik log  --url <base> --repo <owner/name> [--ref main] [--limit 20]
openeik apply <folder | model.json> <commands.json> [--dry-run] [--json] [--out <model.json>] [--replay] [--sequence <n>] [--proposal <name> …]
```

Exit codes: 0 done (and valid, or no difference, or the commands accepted; for `clone`, `push`, `pull` and `status`, done or nothing to do); 1 errors found, differences found, commands refused, a folder refused, a ref moved meanwhile, a remote ref that no longer descends from the tracked version, or a conflict a pull left; 2 a usage or input problem. `clone`, `status`, and `push` and `pull` of a folder with `.eik/remotes.json`, are in [Remotes](#remotes-clone-push-pull). `compile` prints each element's quantities, then its diagnostics and notes; `--json` gives the whole result, bodies included ([Compiling](#compiling)); it exits 1 when a record breaks a rule its derivation needs. `ifc` prints the model as IFC4 (Part VIII, 1) into `--out`, by default `<folder or file name>.ifc` here, and says what it wrote by class and what it left out or approximated, and why; `--json` gives that fidelity report as JSON. A folder's print names its version by its tree's root, as `hash` gives it, unless `--version` names another; without `--at` its time is fixed, so a version prints to the same bytes; `--printed-by` names the organisation that printed it, `--crs` (with `--vertical-datum`) places it on a map, and `--bodies` gives the bodies an application derives, a JSON file of results keyed `elements/<id>` and `types/<id>` ([IFC](#ifc)). `ifcx` prints the model as IFCX, experimentally, from its IFC4 print, with the same options but `--crs`, into `<folder or file name>.ifcx` by default, and says what IFCX loses of the IFC4 print ([IFCX, experimental](#ifcx-experimental)). `apply` writes the new model only with `--out`: as one file when the path ends in `.json`, else as a folder through the writer ([Commands](#commands)). `pull` writes a version into an empty folder, or with `--force` into an existing one, where it removes only the files of records that are not in the version and leaves every other file alone (Part III, 13).

`merge` lists the conflicts by record and member and the merged model's errors by record; it exits 1 when a conflict is left or the merged model does not check. `merge` and `diff` also say which top folders each change touches (`architecture`, `systems`, `types/systems` …) and name a change to the datums, the project, levels and grids that every discipline shares. `datums` writes a building's datums into a new folder, for a discipline to start its own model from ([Working across disciplines](../../guides/disciplines.md)). `clash` says where the pipes and ducts run into the bodies of the architecture and the structure, through an element or grazing it, from the bodies `compile` derives (`clashModel` in the library); it exits 1 when one does. It is a coordination aid, not a rule of the specification. `--out` writes the merged model, and only when it merged cleanly and checks: as one file (the single-file form, in the stored form) when the path ends in `.json`, otherwise as a folder, through the writer.

## The library

| Function | Does |
|---|---|
| `readFolder(source, {check, lenient, assumeFormat, schemas})` | A folder (a path in Node, or a map of paths to texts or bytes) into the single-file form, with `records`, `kept`, `skipped`, `diagnostics` and `get`, `pathOf`, `hashOf` by list and id. Refuses a folder that does not check by throwing an `OpenEikError` (`code: "folder-refused"`, `diagnostics`) unless `lenient`; but for the layout of its files: a record that is I-JSON in another layout than the stored form is read, and the check's finding on its layout is a warning there (Part III, 5, rule 6). A record or `eik.json` that is not UTF-8, names a member twice, holds a number that is not finite or that expresses more magnitude or precision than a double provides, or nests deeper than 256 levels, is refused with or without the check ([codes](#diagnostic-codes)). With `check: false` it reads a folder that fails only the whole-model check (Part III, 12, rule 5) as it is, and refuses besides only what no reader reads, the list of Part III, 12: `eik.json` that is not JSON, has no `format` or names a version or units it does not implement, a project of another `schemaVersion`, units or up axis, two records of one id in a list Part I, 6, rule 1 names (elements, types, views, levels, grids, sheets and linked references), a file that is not UTF-8 or not JSON, and a kept file in a record's way |
| `readModel(source, {check, lenient, schemas, name})` | The single-file form (Part III, 11), whatever its file is called (11.1, rule 3): its file's bytes, its JSON text or the object, read as `readFolder` reads a folder: `{model, diagnostics}`. Its bytes are read as UTF-8, never with U+FFFD in place of what is no text, and its text as I-JSON, as `readFolder` reads a record, with or without the check. It checks the model whole (`checkModel`) unless `check: false`, and refuses it by throwing an `OpenEikError` (`code: "model-refused"`, `diagnostics`) unless `lenient`; without the check it refuses besides only what no reader reads: bytes that are not UTF-8, text that is not JSON, a number that is not finite, a value that is no object, a schema version, units or an up axis it does not implement, two records of one id in a list Part I, 6, rule 1 names, and an entry of `extras` in a record's way (at its path, its leaf or a folder it needs: Part III, 7, rule 4) |
| `modelFiles(model, {eik, agents, from})`, `writeFolder(dir, model, {check, eik, schemas})` | The writer (Part III, 13), `readFolder`'s inverse: a model in the single-file form as its folder's files, path to text, every JSON file in the stored form; and (Node) onto a folder: checked first (`code: "model-refused"`, `diagnostics`), only the files whose bytes change written, the files of records that went removed and no other file, `AGENTS.md` only where there is none. Returns `{written, removed, unchanged}` |
| `foldPath(path)`, `writePlan(files, disk)` | The writer's helpers: a path folded as a case-insensitive, normalisation-insensitive file system sees it (each name's fold, Part III, 4.3: in NFD, its case folded by the full case folding of Unicode 13.0.0, then in NFC; `foldName` and `caseFold` fold a name and its case), and the plan of a write from a listing of what is on disk (what to write, rename, remove and refuse), so a write does the same on every file system |
| `modelPaths(model)` | The paths a model in the single-file form is written at, in the writer's order, each with the JSON pointer of what it holds: `eik.json`, the project's files, every record's file and a view's detail chunks (Part III, 4 and 10), and the kept files. The validator compares them as the writer does, name by name where case and Unicode normalisation are ignored (Security and privacy, 6.2) |
| `checkFolder(source, {schemas, quality, proposals})`, `checkModel(model or path or JSON text or bytes, {schemas, name, quality, proposals})` | The validator: `{ok, errors, warnings, infos, diagnostics, files, counts}`. With `quality: true`, also the model-quality warnings ([below](#model-quality)); with `proposals: ["electrical"]`, for a model of 0.1, also a proposal that 0.2 makes a rule ([below](#proposals)) |
| `checkQuality(model, {name})`, `QUALITY_RULES`, `namedCategory(name, category)` | The model-quality warnings of a model in the single-file form (one `readFolder` or `readVersion` gave), without the validator; the rules; the category a name names |
| `loadSchemas(source?)`, `schemasOf(format, schemas?)` | The bundled schemas, or a `schemas/` folder, or a map of them; and the set a folder or a model of a format is held to, the set of 0.2 (`of02()`) for a format of 0.2, which names each schema by its path there (`name`) |
| `canonical(value)`, `storedForm(value)`, `hashOf(text)`, `hashValue(value)`, `keptObject(text)` | RFC 8785 canonical JSON, the stored form of a file, SHA-256 of a text, a record's hash, a kept file's object. A value nested deeper than 259 levels (a model that reads holds a detail item of a 256-level chunk three levels deeper) is a TypeError, as a number that is not finite is |
| `segment(id)`, `unsegment(name)`, `placeOf(leaf)`, `recordPlace(path)`, `keptPath(path)`, `escapable(name)`, `foldedKept(fixed, kept)` | The layout (Part III, 2 to 7): an id as a path segment, escaped as Part III, 2 says, and a name read back, every escape decoded in one pass; a leaf's or a file's record, its id read from its name; whether a path is one Part III, 7, rule 1 lets a kept file have. Rule 8 keeps more paths out: `escapable` is true of a name it keeps out (one Part III, 2's rules 2 to 5 would escape; a `%` is none), and `foldedKept` gives the kept files a reader leaves alone, by their paths, with the path each meets: one that folds to one with a record's path, `eik.json`'s, `AGENTS.md`'s or an earlier kept file's, or that passes through `eik.json` or `AGENTS.md`. `folderFiles`, `readFolder`, the validator and the writer apply both |
| `extensionPrefix(name)`, `extensionAt(pointer)`, `isPrefix(name)`, `isReservedName(name)`, `RESERVED_NAMES` | Extensions (Part I, 11): the prefix of an extension member's name, or of the member a JSON pointer ends at; whether a name has a prefix's form; whether it is reserved; the names OpenEik reserves, as `registry/extensions.json` lists them (`eik-*` for every prefix that starts with `eik-`) |
| `TYPE_PATHS` (`@openeik/tools/check`) | The members that name a type, as the schemas define them: an element's `typeId`, `recesses[].typeId`, `edges[].typeId` and `trussLayout.familyTypeId`, and a view's `assembly.typeId`. The validator's check of unused library records and the command processor's `type.delete` follow these and no others |
| `versionTree(source, {eikJson, proposals})` | `{root, objects, leaves, records, kept}`: the folder as content-addressed objects (Part VI, 1 and 2), shards past 4096 entries; with `proposals: ["electrical"]`, each circuit a record's leaf ([Proposals](#proposals)) |
| `filesOfTree(source, root, {only})`, `listLeaves(…)`, `folderEntries(source, tree, path)`, `parseTree(text, hash, where)`, `memorySource(objects)` | A tree back to files; a folder's entries, its shards read through; a tree object's entries; any object source. A tree whose entry is malformed, whose entry name is no path segment that stays in its folder, or that names two entries alike is refused (`code: "not-a-tree"`, `"tree-name"` or `"tree-duplicate"`, the last two with a diagnostic naming the tree), before anything of it is written. `listLeaves`, `folderEntries` and `filesOfTree`, which read a tree as a folder's, also refuse an entry that no folder gives (`code: "tree-name"`): a hidden name, a `~` that is no shard, a shard that holds a shard, or a name in a shard its hash does not give it; `parseTree`, which reads one tree object alone, does not. `filesOfTree` refuses a version that holds a kept file that a reader of a folder leaves alone or reads as another file: at a path no kept file may have, over 1 MiB or holding NUL, at `AGENTS.md` or at a record's path (`code: "tree-kept"`, a diagnostic naming each such leaf). `listLeaves` and `filesOfTree` take `repair: {unplaced: [], diagnostics: []}`, as `readVersion` with `repair` passes it: no such tree is refused, each such entry is reported and left out ([Reading a version that no folder gives](#reading-a-version-that-no-folder-gives)) |
| `diffTrees(source, a, b, {proposals})`, `diffFolders(a, b, {eikJson, proposals})` | `{changes, kept}`: by record and field, moves found by id, a view's detail items counted (Part VI, 4); with `proposals: ["electrical"]`, the circuits as records of the list `circuits` |
| `mergeModels(base, ours, theirs, {choices, schemas, proposals})`, `mergeFolders(base, ours, theirs)` | A three-way merge by record and member ([below](#merging)): `{ok, model, conflicts, merged, diagnostics, choices}`; `ok` only when no conflict is left and the merged model checks whole; with `proposals: ["electrical"]`, the circuits a record list merged by id (`circuits/<id>`), and the merged model checked by the proposal |
| `resolveMerge(result, choices)` | Settles conflicts, ours or theirs for each (`{"elements/wall-north": "theirs", "levels": "ours", "extras/<path>": …}`), and checks the merged model again |
| `mergeBase(store, a, b)`, `mergeVersions(store, {base, ours, theirs}, {choices, commit})` | The merge base of two versions, from their commits' parents; a merge of two versions of a store, and with `commit` a merge commit when it is clean |
| `connectStore({url, repo, token, tokenOrigin, fetch})` | A repository in a store that speaks API v0: `refs`, `getRef`, `moveRef` (compare-and-swap), `get`, `has`, `put` (within the contract's limits), `log`, `getMeta`, `putMeta`, `repoInfo`; and for assets (Part XV), `hasAssets`, `putAsset`, `getAsset` and `assetLimit`. The transport is any `fetch`. A token goes only over https or plain http to this computer (`insecure-url` otherwise), and, with `tokenOrigin`, only to that origin (`token-origin` otherwise), both refused before anything is sent ([Tokens](#tokens)) |
| `readVersion(store, refOrHash, {only, cache, check, lenient, repair, verify})` | A version as `readFolder` gives a folder, plus `version`, `commit`, `tree`, `objects` and `leaves`, each leaf read to its object's hash as `versionTree` gives them, for a writer's cache (`hashOf(list, id)` is the leaf's); `only` reads part of it; objects are checked against their hashes and cached, and each record is parsed once. A version whose tree no folder gives is refused, leniently or not, as `filesOfTree` refuses it; `repair: true` reads it all the same and gives back what it leaves out (`unplaced`); `verify: false` takes the objects of a store of the caller's own that checked them as it took them in, without hashing them again ([Reading a version that no folder gives](#reading-a-version-that-no-folder-gives)) |
| `cachedSource(source, cache, {verify})` | An object source that keeps what it read, by hash: each object the source answers checked against its name (`hash-mismatch`), unless `verify: false`, which is refused for a store `connectStore` connects (`verify-remote`) |
| `pushVersion(store, source, {author, name, note, agents, ref, parent, eikJson, check, summary})` | Uploads what the store lacks, writes a commit, moves the ref by compare-and-swap; throws `code: "ref-moved"` with `current` when someone moved it meanwhile, and `code: "tree-name"`, before anything is sent, for a folder whose version would hold a name no reader of versions takes. `agents`: of a folder of 0.2, the agents whose operations the version holds, as envelopes' `by.agent` name them; the commit gets `agents`, each `{name, version?}` once, sorted (`commitAgents`; Part XVII, 4.3, rule 1), and a folder of 0.1, which has none, is refused (`agents-0.1`; the agent goes into the `note`). `push` and `openeik push --agent <name>[@<version>]` take them too, and `openeik log` names them beside the author |
| `clone(store, dir, {ref, remote, url, repo, proposals})` | ([Remotes](#remotes-clone-push-pull)) A ref's head written into an empty or new folder through the writer, and its tracking ref recorded in `.eik/remotes.json`: `{remote, ref, url, repo, version, tree, written}`. A url with credentials, in its user part, a query string or a fragment, is refused (`bad-url`): the url is kept in the folder, and a token never is |
| `push(store, dir, {message, author, note, remote, ref, proposals})` | The folder's changes since its tracked version as one named version on top of it, the ref moved by compare-and-swap from it: `{pushed, version, tree, parent, uploaded, commit}`; `pushed: false` when there is nothing to push; `code: "ref-moved"` (pull first) when the ref moved meanwhile, and nothing written |
| `pull(store, dir, {remote, ref, choices, proposals})` | What others pushed: `{ok, status, base, theirs, version, tree, written, removed, conflicts, merge, diagnostics}`, `status` one of `up-to-date`, `fast-forward`, `merged`, `conflicts` and `refused`; nothing written unless `ok`; `code: "not-descendant"` (with `current` and `tracked`), and nothing written, when the ref no longer descends from the tracked version |
| `status(dir, {remote, ref})`, `fetchHead(store, ref)` | Per remote and ref, the tracked version and whether the folder changed since: `{tree, changed, refs}`, local; a ref's head in one call, to say whether the remote moved |
| `readRemotes(dir)`, `REMOTES_FILE` | A folder's remotes, as `.eik/remotes.json` holds them |
| `recordInputs`, `inputsDigest`, `intentInputs`, `runId`, `resultManifest`, `resultStatus`, `changedInputs` | Result manifests (`openeik-result/0.1`; `resultStatus` reads the earlier `openeik-result/0.1-proposal` as the same format). Pairs are matched by the record they name (`pairKey`), in either layout of Part III, 4.4: `changedInputs` names the inputs that only moved |
| `OpenEikError`, `CODES`, `formatDiagnostic` | Errors with a `code`; the diagnostic codes, this library's own until the specification defines codes (planned for 0.2) |
| `applyCommands(model, commands, {dryRun, replay, sequence, schemas, proposals, answers, version, ref, at})`, `CORE_KINDS`, `ELECTRICAL_KINDS`, `REPLAY_LIFTS` | The command processor (Part V): a batch applied to a model in the single-file form, all or nothing, answered as Part V, 9 says; the kinds it implements, the core record operations and Part XIII, 9's; what a replay lifts ([Commands](#commands)). For a model of 0.2, by every rule of 0.2, each command held to the schemas of 0.2: a batch in an envelope, every refusal coded, the categories 0.2 adds made and changed by their rules, and Part XIII, 9's kinds and cascades; for a model of 0.1, so with `proposals: ["envelope"]`, `electrical`, `member` or `provision`, every whole-model check with the proposals asked for ([Proposals](#proposals)) |
| `COMMAND_CODES`, `ENVELOPE_FORMAT`, `MODES`, `PRECONDITIONS`, `ADDED_PRECONDITIONS`, `isEnvelope(value)`, `operationDigest(envelope)` | Part XVII, commands with an envelope ([Proposals](#proposals)): the codes of the processor's own refusals, with their rules ([below](#command-codes)); the envelope's format and modes; the precondition each kind takes in one, and the fourteen kinds that had none; whether a value is an envelope; its digest. Also `@openeik/tools/envelope` |
| `compileModel(model, {proposals})` | The compiler (Part IV, the class-A rules): `{producer, bodies, quantities, lines, sources, diagnostics, notes}` for a model in the single-file form; with `proposals: ["electrical"]`, also Part XIII's bodies and `legs` ([Proposals](#proposals)). The model is not changed, and the order of its elements does not matter ([Compiling](#compiling)) |
| `derivedLayer(model, result)`, `derivedCurrent(model, file)` | The optional derived layer (Part IV, 1.1, in the layout and form 1.1.2 proposes for 0.2): one result per element for `.eik/derived/elements/<id>.json`, naming every record it comes from by hash, its bodies in 1.1.1's kinds; whether such a result may still be used |
| `readDerived(model, files, own?)`, `derivedBodies(body)`, `derivedPath(key)`, `derivedKey(path)`, `DERIVED_CODES`, `DERIVED_FOLDER`, `DERIVED_LISTS`, `DERIVED_SCHEMA` | A folder's derived layer read (Part IV, 1.1.2): the results a reader may use, by key, and why it uses no other file ([Compiling](#compiling)); a compiler's body in 1.1.1's kinds; a result's path from its key, and its key from its path |
| `sectionProperties(section)`, `sectionCells(section)` | A member section's exact area and centroid offset (Part IV, 6.5.5); its convex cells, arcs drawn with chords |
| `COMPILE_CODES`, `PRODUCER` | The compiler's diagnostic codes; its name and version, as a derived result names its producer |
| `ifcOfModel(model, {version, at, printedBy, proposals, crs, bodies, sets})` | The IFC bridge (Part VIII, 1): `{text, report}`, a model in the single-file form printed as IFC4, from the bodies `compileModel` derives, the bodies an application gives (`bodies`) and the bridge's stand-ins where neither is, and its fidelity report. `version` is `{id?, root?, at?}`, named in the print's `OpenEik_Source`; `at` the print's time; `printedBy` `{organisation}`, who printed it; `crs` `{epsg, verticalDatum?}`, a projected system to place it on a map; `bodies`, results of the derived layer keyed `elements/<id>` and `types/<id>` ([Bodies an application gives](#bodies-an-application-gives)); `sets`, an application's own property sets, keyed so ([IFC](#ifc)). No clock and nothing of Node.js: the same model and options give the same bytes, in a browser or a worker as in Node ([IFC](#ifc)). Also `@openeik/tools/ifc` |
| `projectedCrs(epsg)`, `transverseMercator(latitude, longitude, crs)`, `KNOWN_CRS` | The projected systems the bridge places a print in (SWEREF 99 TM and its local zones, EPSG:3006 to 3018; ETRS89 / UTM 28N to 38N; WGS 84 / UTM 1N to 60N) by their published parameters, and a latitude and longitude projected by Krüger's series: `{easting, northing, convergence}`, metres and degrees |
| `ifcxOfModel(model, options)` | EXPERIMENTAL: the IFCX print (Part VIII, 1, its note on IFCX; open questions, 86), `{text, report}`, buildingSMART's `ifcx_alpha`, which will change with its post-alpha format, read from the IFC4 print of `ifcOfModel` with the same options: the same objects, each path its GlobalId as a UUID, classes, predefined types, properties and bodies, as meshes in metres; its report the IFC4 print's, with what IFCX loses of it (`lost`). No clock and nothing of Node.js, as `ifcOfModel` ([IFCX, experimental](#ifcx-experimental)). Also `@openeik/tools/ifcx` |
| `uuidOfGlobalId(globalId)`, `uuid5(namespace, name)`, `IFCX_VERSION`, `IFCX_EXPERIMENTAL`, `IFCX_IMPORTS`, `IFCX_IMPORTED_KEYS`, `IFCX_OWN_SCHEMAS` | A GlobalId as the UUID its 128 bits are, as buildingSMART's converter keeps it; a name-based UUID of version 5 (RFC 9562), a body's path; the version of IFCX written (`ifcx_alpha`), the sentence that says it is experimental, buildingSMART's schemas a print imports and the keys each defines, and the schemas of the print's own keys |
| `readStep(text)`, `StepError` | A reader of ISO 10303-21 text: its header and its instances by id, every string escape of the standard and complex instances; what the IFCX print reads the IFC4 print with, and `tools/ifc-validate.mjs` diffs prints with |
| `eikOfIfc(text, {mesher, producer})` | Reading IFC into OpenEik (Part VIII, 2, of 0.2; in 0.1 the proposal `ifc-read`): an IFC4 or IFC2X3 file read once, to start a project, async, `{model, bodies, report}`: the single-file form, each element's shape a given body (`shape: "given"`), the bodies as results of the derived layer keyed `elements/<id>` and `types/<id>`, and the fidelity report. Deterministic and without a clock or Node.js; `mesher` meshes what it does not read ([Reading IFC](#reading-ifc)). Also `idOfGlobalId`, `IFC_READ_CLASSES`, `IFC_READ_SCHEMAS`, `IFC_READ_RULE`, `IFC_READ_MESHER_RULE`, `PLACEMENT_LENGTH`, and `IFC_READ`, `givenShape` and `ifcReadMembers`, what the proposal adds to records |
| `ifcGuid(projectId, key)`, `stepString(text)`, `printSentence(root)`, `IFC_SCHEMA`, `IFC_BRIDGE`, `IFC_FIXED_TIME`, `IFC_PRINTED_BY`, `IFC_SOURCE_PSET`, `IFC_REPORT_DOCUMENT` | A GlobalId derived as Part VIII, 1, rule 3 asks and the reference bridge derives it (`element:<id>`, `type:<IFC class>:<type id>`, `level:<id>`, `opening:<id>`, `stair-part:<id>:<piece>`, `port:<id>:<index or end>`); a text as an ISO 10303-21 string; the sentence that says a file is a print; the schema written (`IFC4`), the bridge as a print names it, the time of a print given none, who printed it given no one, the name of the property set that names the version and of the document that carries the report's short form |
| `bodyShells(body, dz)`, `shellsClosed(shells)`, `shellsVolume(shells)` | A body of the compiler (Part IV, 8) as faceted shells, plane faces with their holes turned outward, as the bridge writes them; whether shells are closed; the volume they hold |
| `doorOperation(element, type)`, `windowPartitioning(type)` | How a door opens and how a window is divided, as the bridge writes them on a door's or a window's IFC type (Part II, 6.5; Part VIII, 1.2): `{value, label?}`, an `IfcDoorTypeOperationEnum` or an `IfcWindowTypePartitioningEnum` value and, for `USERDEFINED`, its text; for an application that writes its own IFC and wants the same values |
| `facingOf(model, id)`, `roofWindowFacing(model, roofId, windowId)`, `wallFacing(wall)`, `bearingOf(facing, trueNorth)`, `northOf(model)` | Which way an element faces (Part II, 6.8): a wall's, a door's or a window's facing in plan and its bearing, clockwise from true north; a roof window's facing, tilt and bearing, none where it lies level; true north, the site's or plan +Y. Also `@openeik/tools/facing` |
| `trimmedOutlineOf(model, id)`, `edgeTrimProblems(model, id)`, `edgeTrims(element, hostOf)`, `trimHost(wall, type)`, `trimmedOutline(outline, faces)`, `wallFace(a, b, T, face)`, `clipOutline(outline, f)`, `TRIMMED`, `TRIM_ANGLE`, `TRIM_REACH` | Outline edges trimmed to a wall's face (Part II, 6.7; Part IV, 4.5), the functions the validator and the compiler apply: the outline a slab's, foundation's, ceiling's or roof's bodies are built on, `{outline, area, trimmed, trims}`, without compiling; the validator's findings on one element's trims, in its words (a round footing's, and an outline the faces would leave too small, which `trimmedOutlineOf` leaves untrimmed); each entry of `edgeAttachments` with its face, or the rule it breaks; what an entry reads of its wall, from a model of any shape; an outline cut at faces; a wall's face as a line; an outline cut along a line. Also `@openeik/tools/trims` |
| `PROPOSALS`, `ELECTRICAL_CATEGORIES`, `ELECTRICAL_CODES`, `LIGHT_KINDS`, `LIGHT_MEMBERS` | The proposals this package implements (`electrical`, `drawings`, `ownership`, `assets`, `envelope`, `review`), the categories Part XIII adds, its diagnostic codes, and a luminaire's kinds and light output members ([Proposals](#proposals)) |
| `folderAssets(folder)`, `readPointer(leaf, text)`, `pointerText(sha256, bytes)`, `assetsOfTree(source, root)`, `sendAssets(store, assets)`, `fetchAssets(store, pointers, have)`, `mergeAssets(base, ours, theirs)`, `namedAssets(model)`, `ASSET_KINDS`, `ASSET_MOST`, `ASSET_CODES` | Part XV, assets ([Proposals](#proposals)): a folder's assets and the files of `assets/` that are none; a version's pointer read back and made; a version's pointers; the bytes a store lacks sent, and a version's fetched and checked; assets merged file by file; the assets a model names; the kinds, the size limit and the codes. Also `@openeik/tools/assets` |
| `DRAWINGS_CODES`, `DRAWINGS_SVG_CODES`, `DRAWINGS_LATER_CODES`, `DRAWING_FORMATS`, `drawingsChecks`, `drawingFileOf`, `personIdForAccount`, `checkDrawingSvg`, `drawingGroups`, `readDerivation`, `diffDrawingSvg`, `drawingStatus`, `drawingImpact`, `verifyDrawingSignoffs`, `drawingReviewBcf`, `shapeProblem`, `SHAPE_TOLERANCE` | Part XIV, drawings: the codes of its static checks, of the SVG profile, and of the checks that need cryptography or the history; its files' formats; its checks; the issued SVG's profile, groups, derivation and comparison; freshness; the sign-offs' verification; a review exported as BCF 3.0; and why a review item's shape breaks its form (XIV.13.4, rule 9) ([Proposals](#proposals)) |
| `REVIEW_CODES`, `REVIEW_LATER_CODES`, `REVIEW_ANCHOR_CODES`, `MODEL_REVIEW_FORMAT`, `MODEL_REVIEWS_FOLDER`, `TERMS_FOLDER`, `ISSUE_PREDICATE_TYPE`, `RECEIPT_PREDICATE_TYPE`, `TERMS_PREDICATE_TYPE`, `reviewChecks`, `reviewFileOf`, `reviewNamedAssets`, `modelReviewPath`, `issueRecordPath`, `modelReviewBcf` | Part XVIII, review and issue: the codes of its static checks, and of those that need the reviewed version, the history or cryptography; its files' format, folders and predicate types; its checks; what a path is of its files; the assets a model's reviews name; and a model review exported as BCF 3.0 ([Proposals](#proposals)). Also `@openeik/tools/review` |
| `verifyReviewStatements(folder, {hostKeys, tsaCerts, now, kinds})`, `verifyReceipts(folder, {hostKeys, tsaCerts, now})`, `readHostKeys(text or bytes)`, `HOST_KEYS_FORMAT`, `HOST_KEYS_PATH`, `HOST_KEYS_SCHEMA` | Part XVIII's statements verified offline (XVIII.4.4; 5.3, rule 7; 6.2, rule 7): an issue's statements, a host's receipts and terms accepted, each signature with its key, a person's of the registry or a host's of the hosts' files of keys given (XVIII.5.6), its RFC 3161 time-stamp chained to a certificate given, and the key's window, then its subjects and predicate type; by the statement's path, each `{ok, problems, warnings, kind, issue, host, hostKeys, keyid, time, …}`, `hostKeys` the SHA-256 of the host's file that verified it. `readHostKeys` holds a host's file to its schema and rule 5.6.4 ([Proposals](#proposals)) |

Types are in `types/` (generated from the JSDoc with `npm run types` in this folder). Each module can be imported on its own (`@openeik/tools/canonical`, `/check`, `/compile`, `/quality`, `/read`, `/write`, `/tree`, `/diff`, `/merge`, `/migrate`, `/disciplines`, `/clash`, `/facing`, `/trims`, `/ownership`, `/assets`, `/envelope`, `/store`, `/remote`, `/result`, `/schemas`, `/commands`, `/electrical`, `/drawings`, `/review`, `/ifc`, `/ifcx`, `/ifc-read`). `readFolder`, `readModel`, `modelFiles` and `writeFolder` take `proposals` as the validator does ([Proposals](#proposals)).

## Commands

`applyCommands` changes a model by typed commands (Part V), never in place: it works on a copy and gives the new model back when the batch is accepted. Each command is checked in the three stages of Part V, 6: its shape against its kind's schema of the model's version, `schemas/0.2/commands/<kind>.schema.json` for a model of 0.2 and `schemas/commands/<kind>.schema.json` for one of 0.1, the rules of its kind (the target exists, the preconditions hold, references name records of the right kind, the patch and delete rules), and, once after the last command, the whole model with `checkModel`, by the model's version: a model of 0.2 by every rule of 0.2, one of 0.1 with the proposals asked for (`proposals`). A refusal names the command by its index, kind and id.

```js
import { readFolder, applyCommands } from "@openeik/tools";

const { model } = readFolder("examples/one-storey-house");
const answer = applyCommands(model, [
  { kind: "update", id: "wall-west", expectedRevision: 1, patch: { name: "West wall (render)" } },
], { dryRun: true });
// {ok: true, dryRun: true, results: [{index: 0, ok: true, kind: "update", result: {…the wall…}}],
//  affected: ["wall-west"], verify: {filter: {elementIds: ["wall-west"]}, snapshot3d: {…}, snapshot2d: [{viewId: "view-plan-0", …}]}}
applyCommands(model, [{ kind: "update", id: "wall-west", expectedRevision: 0, patch: { name: "W" } }]);
// {ok: false, dryRun: false, failed: 1, results: [{index: 0, ok: false, kind: "update", commandId: "wall-west",
//  error: "Command 0 (update wall-west): Revision conflict: expected 0, current 1. Read and retry."}]}
```

- **Kinds**: the 30 core record operations of Part V, 2.1 (`CORE_KINDS`); and, for a model of 0.2, `view.upsert` of the documentation profile (Part V, 2.2; `DOCUMENTATION_KINDS_IMPLEMENTED`), a view written whole, a new one counted as revision 0, and Part XIII's three of section 9, `circuit.upsert`, `circuit.delete` and `view.lighting` (`ELECTRICAL_KINDS`; [Proposals](#proposals)). A kind is implemented for a model whose version's command schemas list it (`x-eik-commands`): those of 0.1, as the tag `v0.1.0` keeps them, list the core record operations alone, so a model of 0.1 refuses the other four as not implemented, with the proposal electrical or without it. The rest of the documentation profile and the domain operations are refused as not implemented; send a domain operation as the core operations it leaves (`wall.attach` as an `update` of `top`). What needs a copy's elements worked out, which the specification does not define, is refused (`original.frame` to another instance, `original.edit` at a copy, `copy.move` of the kept instance, `copy.unique` of a copy).
- **Unknown members**: a member a kind does not define at the top level is refused, with what it may have meant; a member the record schemas do not know, in a record or a patch, is kept, with a warning, and an extension member (`<prefix>:<name>`, Part I, 11.2) is kept without one.
- **The schemas of the model's version** (Part I, 9.4): a command on a model of 0.2 is held to the command schemas of 0.2, `schemas/0.2/commands/`, folded at the cut with every rule of 0.2 (`tools/fold-schemas.mjs`) and written by hand there since: a `create`'s element and a `type.upsert`'s type take every category of 0.2, each held to its category's schema, and every record a command carries to the schema of 0.2 in its place, so that a member a rule of 0.2 adds (a site's `crs` or `trueNorth` alone, an `owner`, `classifications`, a layer's `grain`, a slab's `plane`, a door family's `leafMaterial` …) is a member the schemas know. A command on a model of 0.1 is held to 0.1's, `schemas/commands/`, and, asked for proposals, the record it carries is held as the validator holds it with them (`recordFindings` in `check.js`): the members a proposal adds known and held to their schemas, the site of `project.site` to the proposal site's schema, and a category a proposal adds to its schema.
- **Extension members whose value is `null`**: a command that would write one into a record that did not hold it is refused, naming the record and the member (Part I, 11.2, rule 3; Part V, 1.3). In a record a command writes whole (`create`, the upserts) and in the value a patch gives a member, `null` is a value; a patch's own `null` takes a member away (Part V, 4.2). One a record held, before the command or before the batch, and that the command carries back as it was read, is kept (Part I, 10, rule 1), and so is one in the copy `type.duplicate` makes; a replay keeps one with a warning.
- **Preconditions**: `expectedLevel` is compared with the whole level in the canonical form, every member included, those the schemas do not know and extension members too (Part V, 3.3; Part I, 11.2, rule 5), and a refusal names each member that differs: `Level conflict: level "level-1" is not as the command read it: acme:datum is "N2000" now, and absent from expectedLevel.`
- **What names a type**: `type.delete` is refused while an element names the type by a member the schemas define for it (`TYPE_PATHS`; Part V, 5.4), or a view does in its `assembly.typeId`, since a folder holds every type its records name (Part III, 12). A member no schema defines names nothing: an extension member, or a `typeId` nested in a member the schemas do not know. The validator's `library-unused` follows the same members.
- **Categories**: the 31 of 0.1, and the five 0.2 adds (Part V, 1.5): `electrical`, `cableCarrier` and `cable` (Part XIII), `member` (Part II, 6.2.1) and `provision` (Part II, 6.10), for every model of 0.2, and for a model of 0.1 asked for the proposal that adds them (`electrical`, `member`, `provision`); each record held to the schema `checkModel` holds it to (`categorySchema`) and changed by the proposal's rules ([Proposals](#proposals)). A `generic` element's shape is its declared geometry, its own or its type's, and its type sets none of its sizes; it moves to a category that fits by a `delete` and a `create` with its id, in one batch (Part V, 7.5). No command makes an element or a type of a category the processor does not know; those a model holds are kept as they are, refused an update and removable by a delete (Part V, 1.5).
- **A supplier's parts**: `declaredGeometry` and the railing members of Part II, 12 are written by `type.upsert`, `create` and `update`; a patch replaces them whole, an `update` writes a railing's `posts.at`, and a railing's host and edges must resolve (Part V, 11). No command carries a quote; the specification names no member for a price.
- **Batches**: all or nothing, the whole model checked once after the last command; a failing dry run answers for every command. No clock, no random ids; a command is read as its JSON is stored (NaN refused, a patch's `undefined` refused: send `null`). `replay: true` lifts the admission rules of `REPLAY_LIFTS`.
- **Part XIII, 9** (a model of 0.2; and its deletes in one of 0.1 asked for the proposal electrical, below): `circuit.upsert` writes a circuit whole, a new one at revision 0 and a changed one at its revision plus one, refusing `expectedRevision` for a new one (`null` names it in an envelope); `circuit.delete` removes one and lets go of its devices; `view.lighting` switches luminaires in a 3D view's lighting scene by `ids`, `rooms`, `switches`, `circuits`, `levels`, `kinds` or `all` (a union within a selector, an intersection across them), writes the view with its revision plus one and changes no element, and answers with the view, what it switched (`switched`, each luminaire's level from 0, off, to 100, full) and what selected nothing (`selectsNothing`, each with a warning saying why). A delete takes with it what section 9 says (`removeElements`): a board's circuits (`removedCircuits` in the answer), a circuit's devices let go, their `circuitId` and `from` cleared, and its modelled cables; a device's feeds, each fed from what fed it, its id out of every `controls` (one left empty goes) and the cables to it; a carrier's id out of every cable's `carrierIds` (one left empty goes); and a luminaire's id out of every lighting scene, a scene left with nothing on removed. Each record it changes gets its revision plus one and is `affected`. Within a batch a `delete` may name the revision its element had before such a cascade changed it, and a `delete` of what one took is done (`alreadyDeleted: true`), in a dry run and a replay too. A railing on an outline, a wall's top or a slab edge element gets no profile where Part II, 12.2.1 applies (a model of 0.2, or the proposal railings): only its level and offset follow its host, and a profile a command sends is left out, with a warning.
- **The answer**: `affected` holds what was written or removed and the elements that follow it (hosted, attached, framed, and the walls of changed openings; not joins found from geometry); `verify` filters the affected elements, with a 3D snapshot and up to three plans of their levels.

`openeik apply` does the same from the command line. With `--out` it writes the new model: as one file when the path ends in `.json`, else as a folder through the writer, so `openeik apply ./house batch.json --out ./house` changes only the files the batch changed. With `--proposal`, it reads, checks and writes a model of 0.1 with the proposal, as the processor applies it; a model of 0.2 needs none.

## Versions

This package reads OpenEik 0.1, 0.2, 0.3 and 0.4, each with or without a pre-release suffix, and refuses any other version, naming the one it found (Part I, 9.2, rule 3): `openeik/0.5` and `openeik/1.0` are `format-unsupported`. A model of 0.2 names its format at its top (the single-file form, Part III, 11, rule 5) and is written as 0.2; a model that names none is one of 0.1, written as 0.1 (`openeik/0.1-draft`, `FORMAT`) unless it is asked for 0.2 by `eik: {format: "openeik/0.2"}`, which migrates it as Part I, 9.8 says (`FORMATS`, `WRITES`, `FORMAT`, from `@openeik/tools/versions`). A model written as 0.2 is written by 0.2's rules: in the owners' layout, its circuits and people as records, a `schemaVersion` it gives kept and none added. `writeFolder` does not take a folder of 0.2 back to 0.1 (`format-unsupported`), and says when it migrates a folder of 0.1 to 0.2 (`migrated: {from, to}`); `compileModel`, `ifcOfModel`, `applyCommands`, `mergeModels` and `diffTrees` apply 0.2's rules to a model or a tree of 0.2, and a derived result of one names the rule set `OpenEik 0.2 Part IV, class A`.

The schemas of 0.2 are in `schemas/0.2/` beside those of 0.1, their `$id`s under `https://openeik.org/schemas/0.2/`, and are edited there: `tools/fold-schemas.mjs` folded them at the cut from the schemas of 0.1 and the proposals 0.2 makes rules; since, each one written by hand is changed where it is, and `tools/generate-schemas.mjs` writes the generated ones from the reference implementation, made 0.2's by the same fold, and no other (`schemas/0.2/README.md`). The schemas of 0.1 at the top of `schemas/`, `schemas/proposed/` among them, are byte for byte as the tag `v0.1.0` keeps them. The bundle (`src/schemas.js`) holds both, 0.2's under `0.2/`; `loadSchemas()` gives 0.1's, `loadSchemas(undefined, {version: "0.2"})` and either set's `of02()` 0.2's. `checkFolder` and `checkModel` hold a model of 0.2 to the schemas of 0.2, a schema a check names by its path of 0.1 standing for the one in its place (`at02`, `src/schema-paths.js`), and a model of 0.1 to 0.1's and the proposals asked for.

**The migration of 0.1 to 0.2** ([Part I, 9.8](https://openeik.org/spec/0.2/foundations#9.8)): `migrateModel(model)` gives a model of 0.1 as a model of 0.2, its format and units at its top, its owners file for the owners' layout, a `fire` emergency luminaire an `electrical` one, a toilet or a basin component a terminal, a railing on an outline without its profile, and `findings` for what stays as it was (`sheet-application-record` for a sheet of 0.1 that is no sheet of Part XIV, which stays an application record); `migrateFiles` and `migrateFolder` (`openeik migrate`) migrate a folder in one write, read by every proposal 0.2 makes a rule whether or not one is asked for and judged by the folder of 0.2 it becomes, its records moved to their owners' folders, and say what moved (`moved`), what was rewritten (`rewritten`) and what stays (`findings`).

A model of 0.1 is read by 0.1's rules, and a proposal applies only where it is asked for. A model of 0.2 is read by 0.2's rules: the proposals 0.2 makes rules of the standard (`RULES_0_2`: `electrical`, `drawings`, `ownership`, `assets`, `envelope`, `review`, `frames`, `stairs`, `grain`, `framing`, `site`, `classifications`, `railings`, `declared`, `components`, `rooms`, `framingLayout`, `ifc-read`, `member`, `slopedSlab`, `provision` and `adopted`: every proposal of 0.1) apply to it whether or not they are asked for, in every function that reads a folder, a version or a single-file model. From 0.2, the single-file form names its format and units at its top, as `eik.json` does for a folder, and `readFolder` gives a folder of 0.2 so: `{format, units, project, …}`; a project of 0.2 needs no `schemaVersion`, and one it gives is not read.

**OpenEik 0.3** (the cut of 31 October 2026): a model of 0.3 is read by every rule of 0.2 and the six that 0.3 adds (`RULES_0_3`: `wallRebar`, `wallTopTolerance`, `commitApp`, `materialCategory`, `estimate`, `groupIds`), whether or not they are asked for, and held to the schemas of 0.3, `schemas/0.3/`, the fold of 0.2's and the proposals' (`tools/fold-schemas-0.3.mjs`): `loadSchemas(undefined, {version: "0.3"})` and every set's `of03()`. `proposalsAt(undefined, "openeik/0.3")`, and so for any pre-release of 0.3, gives `RULES_0_2` and `RULES_0_3`, and `ownersLayout` of it is true. A model or a folder of 0.2 migrates to 0.3 by its format alone (Part I, 9.9): `migrateModel(model, {to: "openeik/0.3"})`, `migrateFiles` and `migrateFolder` with `to: "openeik/0.3"`, and `openeik migrate --to 0.3`; a model of 0.1 so goes by way of 0.2. A model written with `eik: {format: "openeik/0.3"}` is migrated in the same write. A version of a folder of 0.3 names its application with `app`, with no proposal (Part VI, 3.5). `FORMAT_0_3`, `RULES_0_3`, `isFormat03` and `from02` (a format read by 0.2's rules: 0.2's or 0.3's) are exported. A model of 0.2 may still ask for any of the six by name, as before the cut.

**OpenEik 0.4** (the cut of 1 November 2026): a model of 0.4 is read by every rule of 0.2 and 0.3 and the seven that 0.4 adds (`RULES_0_4`: `ifcWalls`, `ifcMembers`, `ifcSlabs`, `bearsOn`, `provisionTerminals`, `masonry`, `storeFormats`), whether or not they are asked for, and held to the schemas of 0.4, `schemas/0.4/`, the fold of 0.3's and the proposals' (`tools/fold-schemas-0.4.mjs`): `loadSchemas(undefined, {version: "0.4"})` and every set's `of04()`. `proposalsAt(undefined, "openeik/0.4")`, and so for any pre-release of 0.4, gives `RULES_0_2`, `RULES_0_3` and `RULES_0_4`, and `ownersLayout` of it is true. A model or a folder of 0.3 migrates to 0.4 by its format alone (Part I, 9.10): `migrateModel(model, {to: "openeik/0.4"})`, `migrateFiles` and `migrateFolder` with `to: "openeik/0.4"`, and `openeik migrate --to 0.4`; a model of 0.1 or 0.2 so goes by way of 0.3. `eikOfIfc(bytes, {format: "openeik/0.4"})` reads IFC as a model of 0.4, recognising walls, beams and columns, slabs and foundations (Part VIII, 2.6); without it, a reader asked for none of them writes 0.2, as before. `compileModel` of a model of 0.4 names each derived member's bearings (`bearsOn`, Part IV, 7.4.13.1 and 7.4.15.1), and a result of it the rule set `OpenEik 0.4 Part IV, class A`. `FORMAT_0_4`, `RULES_0_4`, `isFormat04` and `from03` (a format read by 0.3's rules: 0.3's or 0.4's) are exported. A model of 0.3 may still ask for any of the seven by name, as before the cut.

## Proposals

Every part and section that OpenEik 0.1 had as a proposal is a rule of 0.2 (`RULES_0_2`), and this package applies it to every model of 0.2, asked for or not, with 0.2's schemas (`schemas/0.2/`). For a model of 0.1, a proposal applies only when it is asked for (`proposals: ["electrical"]`, `--proposal electrical`), with the schemas of 0.1 and of the proposal (`schemas/proposed/`): without it, nothing changes. Each paragraph below names a part by its proposal's name: "with it" is a model of 0.2, or of 0.1 asked for it, and "without it" a model of 0.1 not asked for it.

**Electrical installations**, [Part XIII](https://openeik.org/spec/0.2/electrical), of 0.2 (in 0.1 the proposal `electrical`): devices and boards (category `electrical`), circuits (`project/circuits/<id>.json`), feed chains, control, a 3D view's lighting scene (`display.lighting`), the legends of electrical drawings (`legend.scheme` `electrical` and `circuits`), cable carriers (`cableCarrier`) and modelled cables (`cable`).

```js
checkFolder("./house", { proposals: ["electrical"] });   // the rules of Part XIII too, with the codes below
readFolder("./house", { proposals: ["electrical"] });    // the circuits read as records: model.circuits
versionTree("./house", { proposals: ["electrical"] });   // each circuit a record's leaf, project/circuits/<id>
await clone(store, "./copy", { proposals: ["electrical"] });  // then push, pull and status carry the proposal
compileModel(model, { proposals: ["electrical"] });      // the devices' bodies, the carriers' and cables' runs, the legs
```

```sh
openeik check ./house --proposal electrical
openeik compile ./house --proposal electrical
openeik read ./house --proposal electrical --out house.json    # and openeik write house.json ./house --proposal electrical
node tools/run-conformance.mjs --version 0.1 --proposal electrical    # the proposal's suite of 0.1, as its builder gives it
```

- **Without it**, the elements and types of the three categories are of categories this package does not know: kept, held to the common schemas, a `category-unknown` warning each and a `member-unknown` for each payload; the circuits' files are kept files, and the member `circuits` of `project/order.json` an `order-unknown` (Part I, 10, rule 5; Part III, 7 and 9, rule 5); a view's `display.lighting` is a member the view's schema does not know, kept, a `member-unknown`; a legend's `scheme` `electrical` or `circuits` is a value 0.1's schema does not hold, a `schema-invalid` error. The compiler gives them no body.
- **With it**, a version's tree holds each circuit as a record's leaf, `project/circuits/<id>`, its object the circuit's canonical JSON, and counts it among the records (XIII.3.1; Part VI, 2, rule 1), as the reference implementation stores it: `versionTree`, `pushVersion`, `readVersion` and `filesOfTree`, `diffTrees` and `diffFolders`, and `openeik hash`, `tree` and `diff` with `--proposal electrical`, so that the root of a model with circuits is the reference implementation's. A remote carries the proposal as it carries assets (`clone`, `push`, `pull` and `status` with `proposals: ["electrical"]`, or `--proposal electrical`, kept as `electrical: true` in `.eik/remotes.json`): a push stores each circuit as a record's leaf, and a clone or a pull writes such a leaf as `project/circuits/<id>.json` in the stored form; the model is read, written and checked by the proposal, and a pull merges the circuits by id, as records (`mergeModels` with `proposals: ["electrical"]`). Without it, a circuit's file is a kept file of the tree, and the root is another. `drawingStatus` takes either root as the folder's at its step 1 (Part XIV, 9.4). The validator holds the three categories to the proposal's schemas (`schemas/proposed/electrical/`) and to the `systems` folder (`path-discipline`), reads each circuit's file as a record (its schema, the stored form, its name made from its id; the list `circuits` of `project/order.json`), holds a view's `display.lighting` to its own schema (`view-lighting.schema.json`), in a folder and in the single-file form, as no unknown member, accepts a legend's `scheme` `electrical` or `circuits`, which the proposal's index names in its `values` (XIII.4.4), and checks the rules of sections 2.3, 3.3, 4.1, 4.3, 5 and 6: an error for a breach of a MUST, a warning for one of a SHOULD and for an id of a lighting scene that names no element, each naming its rule as `XIII.4.1.1`. Of the rules of 0.1, those on declared geometry apply to them as to any category.
- **The single-file form** holds the circuits as the list `circuits` (XIII.3.1), in which a kept file at a circuit's path is in a record's way. With the proposal, `readFolder` and `readModel` read them so, and `modelFiles` and `writeFolder` write them back at their paths, name them in `project/order.json` (whose order has no meaning) and remove a circuit's file when it went. A model read with the proposal is written with it: without it, `circuits` is a member the writer does not know, which it writes into `project/project.json` (Part III, 11, rule 4).
- **The compiler** (XIII.7): a device is its declared geometry, its type's placed at its point (origin at `at`, at `elev(levelId) + offset`, x along the facing), else a box of its type's `width` across the facing (a linear device's own, its length, XIII.2.3.3), `depth` along it and `height`, centred on `at`, from `offset` by its mount (a wall device centred on it, a ceiling device under it, a floor device on it); a carrier and a cable are runs as pipes are (Part IV, 12), without insulation, a conduit and a cable round, a tray, a ladder and trunking rectangular; and `legs` gives each modelled leg, the pieces that name one `to`, and its length, the sum of theirs (XIII.6.3). The routes of the other legs, loads and phase balance are applications' (Part XIII, 8).
- **The command processor** (`applyCommands` with a model of 0.2, or of 0.1 with `{ proposals: ["electrical"] }` or `openeik apply --proposal electrical`): `create`, `update`, `delete` and the type commands work on the three categories (Part V, 1, rule 5; XIII.9), each record held to the proposal's schemas; an electrical type sets its elements' height, and their width but for a linear device's, as the validator decides it (`electricalTypeSizes`; XIII.2.3, rule 3), refused where a command sets another (`electrical-size`); the part's own kinds, `circuit.upsert`, `circuit.delete` and `view.lighting`, are implemented for a model of 0.2, held to the schemas of `schemas/0.2/commands/` (0.1's command schemas, as the tag `v0.1.0` keeps them, have none, so a model of 0.1 refuses them with the proposal too), and a delete takes with it what section 9 says, as the reference implementation cascades ([Commands](#commands)); and every batch's model is checked with the proposal, so that a command that breaks a rule of this part is refused, a circuit written on an outlet (`circuit-board`) and a scene on a plan view (`lighting-view`) among them.

**Drawings**, Part XIV, of 0.2 (in 0.1 the proposal `drawings`): sheets as standard records (`drawings/sheets/<id>.json`), detail items' `provenance` and `anchors`, the drawing profile (`drawings/profile.json`), people and their keys (`project/people/<id>.json`), issued drawings (`drawings/issued/<drawing>/`: `drawing.svg`, `revision.json`, `read-set.json`, `signoffs/<n>.json`), registers, issues, reviews and assets by digest (`assets/<sha256>.<ext>`).

```js
checkFolder("./house", { proposals: ["drawings"] });   // the static rules of Part XIV too, with the codes below
readFolder("./house", { proposals: ["drawings"] });    // the people read as records: model.people
```

- **Without it**, a sheet is the application record it is in 0.1, the people's and the drawing files are kept files while they are text of at most 1 MiB, larger or binary files are left alone (`kept-skipped`), and a detail item's `provenance` and `anchors` are members the schema does not know (`member-unknown`).
- **With it**, the validator holds sheets, people and detail items to the proposal's schemas (`schemas/proposed/drawings/`), reads each person's file as a record (the list `people`), holds an issued drawing's SVG and read set up to 64 MiB and the files of `assets/` as bytes, reads each drawing file by its `format` against its schema and the stored form, and checks every rule of sections 2 to 5 and 7 to 15 it can from the folder alone, and those that need unchanged records where the read set shows they are: an error for a breach of a MUST, a warning for one of a SHOULD, each naming its rule as `XIV.8.5.3`. Each issued SVG goes to `checkDrawingSvg` (`src/drawings-svg.js`), the profile's validator, with what the folder says beside it.
- **The single-file form** holds the people as the list `people`, and the drawing files in `extras`; it holds no assets, so whether one is there is not checked.
- **Not here:** the rules that need the history (`key-removed`, `issued-file-changed`, `register-rewritten` and the like), and verifying signatures, which `verifyDrawingSignoffs` (`src/drawings-verify.js`) does.

**A review as BCF 3.0** (Part XIV, 13 and 18): `drawingReviewBcf(folder, review)` exports a review round, `drawings/reviews/<review>.json`, as buildingSMART's BCF-XML 3.0, whose schemas its files validate against (`src/drawings-bcf.js`); `openeik drawing bcf` writes it to a file.

```js
const { bytes, topics, document } = drawingReviewBcf("./house", "rv-01jc4n2x8q");
// bytes: the .bcf zip; topics: [{item: "i-1", guid: "4032da67-…", status: "New", viewpoint: "3eb31f93-…"}, …];
// document: {path: "drawings/issued/A-101/drawing.svg", sha256: "3b455c40…", embedded: true}
```

```sh
openeik drawing bcf ./house rv-01jc4n2x8q --out review.bcf    # default: <review>.bcf here; --json says what it wrote
```

- **Topics:** one per item, in a folder named by its GUID: the item's `bcf`, which a round trip keeps, else one made from the review's id and the item's (a UUID of version 8, from SHA-256), so that the same review always gives the same topics. Its title is the first line of the item's text, its description the text, its type the item's kind, its status the drawing profile's name of its state (else the state's code), its comments the replies, and its labels the drawing's number, the change item it is about and `proposed` for an agent's item no person has accepted; `extensions.xml` lists them all.
- **The drawing reviewed** travels in the file (`documents.xml`, `documents/<guid>`), named by its SHA-256, where the folder holds it with the review's digest: the current revision's `drawing.svg`, or an unlinked drawing's PDF. A review of an earlier revision names it by its path and digest only.
- **Viewpoints:** an anchored item's elements are selected by their OpenEik ids (`AuthoringToolId`, `OriginatingSystem` `OpenEik`; BCF's `IfcGuid` is optional, and an id is none), the model visible round them, and framed by the orthogonal camera BCF 3.0 asks of every viewpoint, in metres, as the anchor's viewport looks: a plan from its cut, a section or an elevation from its own plane, any other view from the south-east. It frames their boxes (9.3), or the model's where none can be boxed.
- **Bytes:** the same folder and review give the same entries, byte for byte; the zip (`src/zip.js`, no dependency, its own CRC-32) stores them, never deflated, with fixed times, so that every runtime, a browser too, writes the same file. A review that is not in the folder, does not validate or that BCF cannot carry is refused (`review-missing`, `review-invalid`).

**Review and issue**, [Part XVIII](https://openeik.org/spec/0.2/review), of 0.2, from plan 688 (in 0.1 the proposal `review`): a review round on a version of the model in 3D, `project/reviews/<review>.json`; an issue that releases a version and names its prints by digest (`versions`, `renditions` and `terms` in Part XIV's issue); an issue's statements, `issued`, `received` and `withdrawn`, and a host's receipts, beside the issue in `drawings/issues/<issue>/signoffs/` and `receipts/`; and terms accepted, `project/terms-accepted/`. Asking for it asks for the proposal drawings too, on whose registry, profile, issues and statements it builds.

```js
checkFolder("./house", { proposals: ["review"] });       // Part XIV's rules and Part XVIII's, with the codes below
modelReviewBcf("./house", "mr-01jcq7d2v9");               // {bytes, topics, model}: the round as BCF 3.0
await verifyReviewStatements("./house", { hostKeys: [hostKeysText], tsaCerts: [tsaPem] });   // by path: {ok, problems, …}
await checkReviewAnchors("./house", connectStore({ url, repo, token }));   // {diagnostics, checked, unchecked}: XVIII.2.5.4
```

```sh
openeik check ./house --proposal review
openeik review bcf ./house mr-01jcq7d2v9 --out round.bcf   # default: <review>.bcf here; --json says what it wrote
openeik review verify ./house --host-keys host-keys.json --tsa tsa.pem   # exit 1 when a statement fails; --json
openeik review anchors ./house                                 # against the folder's remote; or --url <base> --repo <owner/name>
```

- **Without it**, the part's files are kept files, as every 0.1 reader keeps them; with the proposal drawings alone, an issue's `versions`, `renditions` and `terms` are held to its schema, and nothing more.
- **With it**, the validator reads each file of the part by its `format` against its schema and the stored form, decodes each statement's payload to an in-toto Statement v1 of its predicate type, and checks every rule of sections 2.5, 3.2, 4.3, 5.3 and 6.2 it can from the folder alone (`src/review.js`): an error for a breach of a MUST, a warning for one of a SHOULD, each naming its rule as `XVIII.2.5.7`. A receipt's key is the host's, which the building's registry does not hold.
- **BCF** (XVIII.2.6): one topic per item, each with its own viewpoint: its camera, perspective with its field of view or orthogonal with its height, in metres; its shape as lines; its section as clipping planes, a box as six; its anchors' elements selected by their OpenEik ids; and the version reviewed as the topics' document reference, `version/<root>`. The same folder and review give the same bytes (`src/review-bcf.js`).
- **Verifying** (`src/review-verify.js`, XVIII.4.4; 5.3, rule 7; 5.6; 6.2, rule 7): `verifyReviewStatements` and `openeik review verify` check each statement about an issue, receipt and record of terms accepted as `verifyDrawingSignoffs` checks a sign-off, and with its code (`verifySignatures`): each signature with its key over DSSE's pre-authentication encoding, its RFC 3161 time-stamp chained to a certificate given (`tsaCerts`, `--tsa`), and the key's window at the time-stamp's time; then its subjects against the issue record by its hash, or the terms by their digest, and its predicate type. A statement about an issue, and terms accepted `by` `person`, are signed with a key of the signer's record of the registry; a receipt, and terms attested `by` `host`, with a key of a host's file of keys the caller gives (`hostKeys`, `--host-keys`: `/.well-known/openeik/host-keys.json` as fetched from the host and pinned), whose `host.name` is the statement's. `readHostKeys` holds such a file to `schemas/proposed/review/host-keys.schema.json` and to rule 5.6.4 (each key id once, a public key of its algorithm, no window that ends before it begins); a file that is not taken gives no key, and the command refuses it. Each receipt's result names the SHA-256 of the host's file that verified it, which the caller records as the file it trusted (rule 5.6.6); comparing a pinned file with a later one is the caller's. It reports with the part's codes: a signature, a time-stamp or a key's window that does not verify as `issue-signoff-signature`, `receipt-signature` and `terms-signature`; a key that is not the signer's as `issue-signoff-signer` and `terms-signer` (a receipt's as `receipt-signature`); the subjects as `issue-signoff-subject`, `receipt-subject` and `terms-subject`; and the file as `*-shape` and a missing time-stamp as `*-timestamp`.
- **The version reviewed** (`src/review-anchors.js`, XVIII.2.5, rule 4): `checkReviewAnchors(folder, source)` and `openeik review anchors` read each model review's version from an object source, a store or objects in memory, by its `model.root`, and look each anchor's element up among that tree's element records, whose leaves are their records' hashes (Part III, 6), so no record is read: an anchor's `hash` that is not its record's there is an error, an element that version does not hold a warning, both `model-review-anchor`. A round whose version the source does not hold is listed in `unchecked`, with why, and reported nothing of; one tree is read once for every round of it.
- **Not here:** the rule that needs the history (XVIII.3.2, rule 4); fetching a host's file of keys, which the caller does; and an IFC print, whose bridge is written apart (XVIII.7.2).

**Ownership by folder**, [Part XVI](https://openeik.org/spec/0.2/ownership), of 0.2 (in 0.1 the proposal `ownership`): `project/owners.json` names a project's parties and the paths each owns, by globs, the last matching rule deciding; a change to an owned path needs one owner's approval, or every owner's where a rule says `all`.

```js
checkFolder("./house", { proposals: ["ownership"] });    // project/owners.json checked too, with the codes below
ownersOf(owners, "systems/level-0/duct/d1");            // {owners: ["hvac"], all: false, rule: 2}: the last rule that matches
partiesNeeded(owners, touchedPaths(await diffFolders("./before", "./after")));   // what a change needs, rule by rule
```

```sh
openeik check ./house --proposal ownership
openeik owners ./house                                   # the parties and the rules, the file checked first
openeik owners ./house systems/level-0/duct/d1.json project/levels   # the owners of paths
openeik diff ./before ./after                            # owners: the change needs arch for 1 path (…); hvac for 9 paths
openeik merge ./base ./ours ./theirs                     # owners    theirs needs …, by ours' file
```

- **Without it**, `project/owners.json` is a kept file, as every 0.1 reader keeps it.
- **Globs** (XVI.3) match a version's leaf paths: a record's path without `.json`, a kept file's own. `*` stands for any part of one segment, `**` for any number of whole segments; a glob that ends in `.json` also matches as if the `.json` were not there. The order index `project/order` has no owner.
- **What a change needs** is the owners of what it touches by the ownership of the line it joins, as it stood before: `diff` reads folderA's file, and `merge` reads ours', for what theirs changed against the base. A host holds a change until it has those approvals (XVI.4, rule 4); this package only says what they are.

**Folders that follow owners**, [Part III, 4.4](https://openeik.org/spec/0.2/code-base#4.4), of 0.2, with ownership (open question 85): a record's discipline folder is the discipline that owns it, one of Part XVI's eight, so that an owners file says who owns what by plain globs, `<discipline>/**`, `types/<discipline>/**` and `drawings/<discipline>/**`.

```js
readFolder("./house", { proposals: ["ownership"] });     // either layout, record for record the same model
modelFiles(model, { proposals: ["ownership"] });         // the owners' layout: structure/level-0/wall/w1.json for a bearing wall
modelOwners(model).element(wall);                        // "structure": one function gives a record's owner and its folder
migrateFiles(files);                                     // {files, from, to, moved, rewritten, findings, owners, touched, needs, message}: to 0.2, in one write
migrateFolder("./house");                                // the same, written onto the folder
```

```sh
openeik migrate ./house [--dry-run] [--json]             # a folder of 0.1 to 0.2 in one write (Part I, 9.8); a folder of 0.2 only moves
openeik check ./house --proposal ownership               # each record in 0.1's folder or its owner's (path-discipline)
openeik diff ./before ./after --proposal ownership       # moved  elements wall-1  architecture/… -> structure/…
```

- **The owners** (4.4, rules 1 to 5): an element's, a type's and a view's `owner`, one of the eight, set by hand, wins; else `modelOwners` gives it: `structure` for a framing member, a wall, slab or roof that is `structural`, a stair that carries (`structural`, or a type of concrete or cross-laminated timber), the structural categories and a component that is structural or of a plate's or a cast-in item's type; `ventilation` for ducts and air devices (`airTerminal`, `airType`); `plumbing` for pipes and every other terminal; `electrical` for Part XIII's categories; `site` for terrain, grading, property lines, planting, roads and parking; `architecture` for every other element. A wall, slab, roof or stair type is `structure`'s while an element that bears uses it (`bearingTypes`); a view is `setup`'s unless it says; a sheet's owner is its discipline code's (`sheetCode`, `sheetOwner`). A component of another family, which may be a connector of its writer's library, and a record of a category this package does not know, stay in the folder they were read from.
- **Reading** (4.4, rule 7): with the proposal, `placeOf(leaf, true)` and `recordPlace(path, true)` take both layouts, which never give one path two meanings; `readFolder`, `readVersion`, `versionTree`, `diffTrees` and `mergeFolders` do so with `proposals: ["ownership"]`. A remote carries the proposal as it carries the proposal electrical (`clone`, `push`, `pull` and `status` with `proposals: ["ownership"]`, or `--proposal ownership`, kept as `ownership: true` in `.eik/remotes.json`): a version's records are read in either layout, and a clone and a pull write the owners' layout. Without it, a record in an owners' folder of 0.1's (`ventilation/…`, `drawings/<discipline>/…`) is a kept file, as a 0.1 reader keeps it.
- **Writing**: `modelFiles` and `writeFolder` with the proposal write the owners' layout; onto a folder in 0.1's they move its records, which is the change of layout. An `owner` that is none of the eight is refused (`model-invalid`).
- **The change of layout** (4.4, rule 8): `migrateFiles` and `migrateFolder` move each record whose folder changes, a view with its detail chunks, in the write that migrates a folder of 0.1 to 0.2 (Part I, 9.8), which also names the folder's format `openeik/0.2` and rewrites what 0.2 deprecates; for a folder of 0.2 they move records and nothing else: the project's files, `project/order.json` among them, kept files and files a reader leaves alone stay as they were, and every record's object and hash with them. A folder in the owners' layout moves nothing. Where the folder has `project/owners.json`, `ownersForLayout` rewrites it in the same change (XVI.3.1): each glob that names `systems/`, a view's or a sheet's folder of 0.1 or a category goes, and a rule for each discipline whose parties owned one is added, `disciplineGlobs`, owned by those parties and the parties that lead; a party that names no discipline and loses its globs is reported (`unplaced`), for a person to give it paths. The result names what moved, the paths the change touches, and what it needs by the file as it was (`needs`): a version that every owner approves, or that a party that owns `**` makes.
- **Diff** (Part VI, 4, rule 3): a record whose path alone changed is `moved`, with both its paths, and touches both (XVI.4, rule 1); `byFolder` counts it as moved in the folder it went to.
- **The validator** accepts each record in 0.1's folder or its owner's (`path-discipline` for any other), holds `owner` to its schema (`schemas/0.2/ownership/owner.schema.json`), and warns of a folder in both layouts at once (`layout-mixed`).

**Commands with an envelope**, Part XVII, of 0.2 (in 0.1 the proposal `envelope`): a batch sent in an envelope, `{format, operationId, by, target, mode, intent, commands}`; every refusal with a code, a JSON pointer into the command, a message and a fix where one is known; and a precondition on every command.

```js
const answers = new Map();                               // the operations applied, kept by the caller
applyCommands(model, {
  format: "openeik-envelope/0.1",
  operationId: "0b6f6a4e-2c1d-4f7e-9a51-3d2b8c7e1f00",
  by: { person: "u-7f3c", agent: { name: "an-agent", version: "2.1" } },
  target: { ref: "main", version: null },
  mode: "apply",                                          // or "dryRun", or "propose"
  intent: "Rename the west wall",
  commands: [{ kind: "update", id: "wall-west", expectedRevision: 1, patch: { name: "W" } }],
}, { proposals: ["envelope"], answers });
// {ok: true, dryRun: false, mode: "apply", operation: {id, digest, kept: true}, target: {ref: "main", base: null, version: null, compared: false},
//  results, affected: ["wall-west"], verify, model}
applyCommands(model, [{ kind: "update", id: "wall-west", expectedRevision: 0, patch: { name: "W" } }], { proposals: ["envelope"] });
// …results: [{index: 0, ok: false, kind: "update", commandId: "wall-west", error: "Command 0 (update wall-west): Revision conflict: …",
//  refusals: [{code: "revision-conflict", rule: "V.3.1", pointer: "/expectedRevision", message: "Revision conflict: expected 0, current 1. Read and retry.",
//  fix: {read: [{list: "elements", id: "wall-west"}]}, data: {expected: 0, current: 1}}]}]
```

```sh
openeik apply ./house envelope.json --proposal envelope --json    # an envelope, or a batch answered with codes
```

- **Without it**, nothing changes: an envelope is an object without a `kind`, refused as a command, and an answer carries no codes. `openeik apply` reads an envelope's `commands` as it reads `{"commands": […]}`.
- **With it**, a batch without an envelope is applied as 0.1 applies it, and answered with `mode` and its refusals coded. An envelope is checked against `schemas/proposed/envelope/envelope.schema.json` (`envelope-format`, `envelope-schema`), and answered with its `operation` and `target` too (`schemas/proposed/envelope/answer.schema.json`).
- **Codes** (XVII.10): each refusal carries `{code, rule, pointer, message, fix?, data?, record?}`, in `results[i].refusals`, and the envelope's and the model's errors before the batch in `refusals`. A rule the validator checks too keeps the validator's code (`opening-fit`, `ref-missing`, `type-size`), with the record it is on; the processor's own are below. A fix is a JSON Patch of the command (a misspelt member moved, a size the type sets given or left out, a patch's `undefined` sent as `null`), or the records to read again: a conflict's fix never writes the current revision in.
- **Preconditions** (XVII.9): in an envelope every command gives its kind's (`PRECONDITIONS`); an upsert gives `null` for a record that does not exist yet, and a number only for one that does. The fourteen kinds that had none get one (`ADDED_PRECONDITIONS`); of them this package implements `project.excavation`, `project.precast`, `project.standards`, `original.make`, `copy.place`, `original.frame` and `original.edit`. A replay is not refused for a precondition it lacks.
- **Operations** (XVII.3): with `answers`, a `Map` the caller keeps, an operation applied is recorded by its id with the envelope's digest and its answer (without the model); the same id and envelope again gets that answer back, `operation.repeated: true`, applied once; the same id with another envelope is refused (`operation-reused`). A refusal, a dry run and a proposal are not recorded. Without `answers`, `operation.kept` is `false`, and the preconditions refuse a repeat. A store or a host keeps the record itself, durably and with the change, and refuses an id that comes again while it is being applied.
- **Target** (XVII.5): the model is the newest version of the ref `ref` names, `main` by default, as a processor that keeps one line of work calls it; an envelope for another ref is refused (`target-unknown`). With `version`, the version the model is, the base is compared; a ref that moved is applied to, the preconditions deciding, unless the envelope asks for its base `exact` (`base-moved`). The store moves the ref by compare-and-swap. Every envelope that validates is answered with its `operation` and `target`, refused or not.
- **Versions** (XVII.4.3, rule 1): a version of 0.2 made from operations has the person as its `author` and, in its commit, `agents`, each agent whose operations it holds as `{name, version?}`, once, sorted: `pushVersion(store, folder, {author, agents})` writes them from the envelopes' `by.agent`, and `readVersion` gives them back in `commit`. A version of 0.1 has none.
- **Propose** (XVII.6.3): nothing changes, and an accepted batch gives `proposal`, a proposal file of Part VI, 8.7 (its base's records by hash and revision, `by.app` or the agent, in 0.2 `by.agent` as the envelope gives it (XVII.4.3, rule 2), and its `dryRun` when `at` is given), for the application to keep at `<application>/<model>/proposals/<id>.json`.
- **Not here:** who a person is and what a host lets them do (`person-mismatch`, `not-permitted`, `mode-not-allowed`), operations in progress, size limits and envelopes required: a host's.

**Assets**, [Part XV](https://openeik.org/spec/0.2/assets), of 0.2 (in 0.1 the proposal `assets`): binary files kept by the SHA-256 of their bytes, as Git LFS keeps large files. In a folder an asset is the file `assets/<sha256>.<ext>`; in a version, a pointer at that path, `{"asset":{"bytes":<n>,"sha256":"<sha256>"}}`; its bytes are the store's, beside its objects.

```js
checkFolder("./house", { proposals: ["assets"] });          // the files of assets/ checked too, with the codes below
versionTree("./house", { proposals: ["assets"] });          // each asset a pointer leaf; .assets gives them with their bytes
await pushVersion(store, "./house", { author, proposals: ["assets"] });   // the bytes the store lacks first, then the objects and the ref
await clone(store, "./copy", { proposals: ["assets"] });    // every asset fetched and checked before anything is written
await readVersion(store, "main", { proposals: ["assets"] }); // .assets: the version's pointers, by path
```

```sh
openeik check ./house --proposal assets
openeik clone ./copy --url <base> --repo <owner/name> --proposal assets   # then push, pull and status carry the assets
openeik push ./house --url <base> --repo <owner/name> --author <name> --proposal assets   # a folder without a remote, too
```

- **Without it**, `assets/` is what it is in 0.1: a binary file or one over 1 MiB is left alone (`kept-skipped`), and no version holds it; a folder without assets has the same tree either way. A reader of versions without it reads a pointer as a kept file holding the pointer's text, as it reads any leaf at no record's place; a store that keeps assets refuses that text back under `assets/`.
- **With it**, every file of `assets/` is held as bytes (`folderAssets`): one whose name is not its bytes' SHA-256 and a kind's extension, one in a folder there, one of another kind and one over 64 MiB are no assets, reported and left alone, never in a version (XV.2 and 4). A version's tree holds each asset's pointer, and its root commits to the bytes (XV.3). `readVersion` and `filesOfTree` check each pointer (`asset-pointer`) and keep it out of the files.
- **Push, clone, pull** (XV.6): `pushVersion` asks the store which bytes it holds (`assets/has`), refuses before sending anything when one is larger than the store takes (`asset-too-large`, the store's `limits.assetBytes`), sends the rest (`PUT assets/<sha256>`), then the objects and the ref; a store refuses a ref that would name bytes it lacks (`store-asset-missing`, 409, with the assets). `clone` and `pull` fetch every asset of the version they write, each checked by its SHA-256 and length, before they write anything (`asset-missing`, `asset-mismatch`). A pull's merge keeps the assets added on either side and drops one removed on a side, never one the merged model names (`asset-removed`, XV.3, rule 7). The remote keeps the choice (`"assets": true` in `.eik/remotes.json`), so its next push, pull and status carry the assets too.
- **Withheld** (XV.7): an asset the store has let go is reported, and no file is written for it; the remote keeps its pointer (`withheld`), so the folder's versions keep it.
- **A later version's kind** (XV.4, rule 3): a pointer of a kind this version does not know (`readPointer` gives it `known: false`) is neither fetched nor written by a clone or a pull, as a writer of 0.2 writes no such asset; the remote keeps its pointer (`later`, checked to be a pointer at its name when it is read), and the folder's next push and status write it back unchanged. `openeik pull` without a remote writes none either, and says how many it left.
- **Not here:** the single-file form, which carries no asset (XV.9.1), large meshes (a later kind), and checking what a Part XIV member names against the assets present beyond what the proposal drawings checks.

**A door's and a window's own body**, [Part IV, 4.3.1](https://openeik.org/spec/0.2/compiler#4.3.1), of 0.2 (in 0.1 the proposal `frames`): its frame, its leaves or sashes in their grid and its glass, closed, from its type's `openingFamily` or `windowFamily`, as open question 71 decided on 26 October 2026. It adds no file and no member: the sizes are those of 0.1's families.

```js
compileModel(model, { proposals: ["frames"] });   // each door's and window's frame, sash, leaf and glass bodies, each naming its part
ifcOfModel(model, { proposals: ["frames"] });     // written as its body, a shape aspect and a material constituent per part
```

- **Without it**, a door's or a window's own body is reference behaviour, as in 0.1: the compiler gives it none, and the bridge writes the box of its opening, which its fidelity report calls approximated. Nothing else changes: on the reference implementation's seven examples and this repository's, every print and every compiled result is the same, byte for byte, with the other proposals or without them.
- **With it**, the compiler gives each door and window whose type has a family a body per part (`part`: `frame`, `sash`, `leaf`, `glass`), in its wall, and a `volume` quantity (`IV.4.3.1`); a family whose sizes leave no room has none, and a `frames-fit` warning says why. A door or a window whose type has neither family keeps its body reference behaviour. A declared envelope stands in place of its bodies (IV.11.1). The bridge writes the parts as its body, each an IfcShapeAspect, its material an IfcMaterialConstituentSet, the glass styled as glass; its operation type stays on its type, and it no longer calls it approximated. `checkFolder` and `checkModel` hold a door family's `leafMaterial` to its schema (`wood`, `aluminium`, `steel`; rule 14), so that it is no unknown member, and warn of it on a family of another kind (`leaf-material`).
- **Profiles** (rule 15): a family's `frameProfile`, `mullionProfile` and `sashProfile` (a `windowFamily`'s `frameProfile` and `mullionProfile`), each an outline of a `custom` section (Part II, 5.2) whose bounding box is the width by the depth of the members it shapes. With one, the compiler gives each member it shapes as a `sweep` of that section: the frame's and a sash's members the ring round their outline, mitred at the corners (`cuts`), a door's jambs square on its bottom, and each mullion and transom as wide as `mullionWidth` from end to end; a mullion of its own width, and a shaped opening's mullions and sashes, keep their boxes. The volume is exact, each member's area along the line through its centroid. A profile that breaks Part II, 5.2's rules of an outline, whose box is not its members', or on a frame that follows its wall is an error at the type (`frames-profile`), and the compiler gives such a door or window no body of its own (`familyProfiles`, `PROFILES`, `PROFILE_SCHEMA`).
- **Against the reference** (`test/fixtures/frames-bodies.json`, from `tools/frames-reference.mjs`): a probe of 18 doors and windows of every kind, each part's volume and box; a window's parts and every frame agree exactly, and a leaf within the clearances 4.3.1 leaves to the product (8 mm in its box, 2 % in its volume).

**A stair's numbers and layout**, [Part IV, 7.3](https://openeik.org/spec/0.2/compiler#7.3), of 0.2 (in 0.1 the proposal `stairs`): every straight stair's, and every L's and U's with a landing, risers, going, location line, treads, nosing lines and landings, and a cast concrete stair's flights and landings, as open question 76 decided on 26 October 2026. It adds no file and no member.

```js
compileModel(model, { proposals: ["stairs"] });   // result.stairs: each stair's numbers, flights (treads, nosings) and landings; a concrete stair's bodies
ifcOfModel(model, { proposals: ["stairs"] });     // a concrete stair's IfcStairFlights and IfcSlab (LANDING) by rule
```

- **Without it**, stair runs and landings are reference behaviour, as in 0.1: the compiler lays out nothing, and the bridge writes its stand-ins, which its fidelity report calls approximated. Nothing else changes: on the reference implementation's seven examples and this repository's, every print and every compiled result is the same, byte for byte, with the other proposals or without them.
- **With it**, the compiler's result has `stairs`, one entry per straight, L or U stair it lays out: its `numbers` (risers, riser height, going, treads, width, base and top), its `flights`, each with its treads (numbered from 1 up the stair, each its plan outline, its top and its nosing line, from left to right looking up), and its `landings` (outline and top). A cast concrete stair (`construction` `concreteCast`) has its bodies too: each flight its steps down to its soffit, named `part` `flight` and `piece` `flight:<k>`, and each landing a slab, `landing` and `landing:<k>`, and a `volume` quantity (`IV.7.3.6`). A stair it cannot lay out has none, and a `stairs-layout` warning says why; another construction's parts, winders, spiral and helical stairs and `repeatOf` are reference behaviour. The bridge writes a concrete stair's flights and landing as its parts, named and numbered as its stand-ins were, and no longer calls them approximated; another construction's stays approximated. `checkFolder` warns of a stair it cannot lay out.
- **Against the reference** (`test/fixtures/stairs.json`, from `tools/stairs-reference.mjs`): a probe of eight stairs, every number, tread, nosing line and landing, and every concrete flight's and landing's volume and box, exactly.

**A framed wall layer's members, a roof's trusses and a floor's joists**, [Part IV, 7.4](https://openeik.org/spec/0.2/compiler#7.4), of 0.2 (in 0.1 the proposal `framingLayout`): the plates, studs, end and corner studs, a T's backing, each opening's jacks, kings, headers, sill and cripples, the noggings, battens and heel members of each layer of a wall whose type gives it a `framing` rule; each roof's truss layout, its trusses, a roof opening's trimmers, headers and dormer rafters, a ridge beam and its posts, and the walls it bears on; and each floor's joists, trimmers, rim joists, headers and blocking, as open question 83, part 2, decided on 29 October 2026. It adds one member, a horizontal rule's `battens` (`service`, `nailing` or `counter`), and no file.

```js
compileModel(model, { proposals: ["framingLayout"] });   // result.framingLayout: each framed wall's layers, roof's trusses and floor's joists, each member with its bodies
ifcOfModel(model, { proposals: ["framingLayout"] });     // each member under its wall, roof or floor, keyed framing:<id>; each truss an IfcElementAssembly TRUSS
```

- **Without it**, timber framing is reference behaviour, as in 0.1: the compiler lays out nothing, and the bridge writes the members an application gives (`bodies`), or none. Nothing else changes: this repository's examples print the same, byte for byte, with the other proposals or without them.
- **With it**, the compiler's result has `framingLayout`, one entry per framed wall: its `layers`, each its top's `mode` (`raking`, `levelPlate` or `underFloor`) and its `members`, each with its `id` (`<wall>:<layer>:<role>:<key>`), `role`, IFC `class`, `predefined` and `objectType`, `name`, `section`, `material`, `length`, `line`, its `outline` in the layer's elevation and the `across` it lies between, its `volume` and its `bodies` (prisms, cut by the faces the layer is cut by); and the records it comes from (`sources`). It finds the corners and tees it needs by Part IV, 7.4.2 (`compile-framing-joins.js`), and changes no wall's own body. Each roof with a `trussLayout` has an entry too (`compile-framing-roofs.js`, Part IV, 7.4.13): its `trusses`, each with its `id` (`<roof>:truss:<key>`), `mark`, `name`, `position`, its line from `a` to `b` at its bearing plane (`elevation`), its `span`, the `parameters` it gives its family, its `bearings` and its `seat` on each side; its `connections`, an anchor pair where a truss rests and a hanger where a hip truss or a jack hangs; and its `members`, each truss's family parts (`truss`, keyed `<truss id>:<part id>`) and the headers, dormer rafters, ridge beam and posts, each a box of eight corners. The walls a layout bears on are laid out with its trusses (7.4.14): studs under them (`alignToTrusses`), their bottom chords across a batten layer, the heel zone they hold, the heel plate at their seat, and a ridge beam's pocket and post in a gable. Each floor whose type has a layer's `joists` has an entry (`compile-framing-floors.js`, 7.4.15): its `joists` (its layer, the joists' `direction`, the layer's `z`, its `bearings` and the `posts` through it) and its `layers`, whose members are its joists, trimmers, rim joists, headers and blocking. A layout of two sides frames its hip ends (`compile-framing-hips.js`, 7.4.16): a girder of plies where the ridge ends, hip trusses of plies up the hip lines and jacks square to the end and the sides, or a girder along a short ridge with half trusses hanging on it, the hip trusses and jacks of the layout's jack family and the girder along the ridge of its flat-top family, both made of its own family where the model holds none (`<family type>-jack`, `-flat`); each such truss has its `role` (`girder`, `hip`, `jack`), its `family` (`jack`, `flatTop`), its `hipEnd` and its `ply`. A wall whose members, or a roof whose trusses, are materialised is not laid out (`framing-layout-materialised`); a top, a floor, a rule, a truss layout or a joist rule it cannot take is said (`framing-layout-top`, `framing-layout-floor`, `framing-layout-rule`, `framing-layout-roof`, `framing-layout-joists`), and what a layout says is an info (`framing-layout-note`). The bridge writes each member under its wall, roof or floor (IfcRelAggregates), keyed and typed as the reference bridge keys them, its material `Timber <material>` (a nail plate's steel); each truss as an IfcElementAssembly TRUSS with its members under it; each anchor pair as an IfcMechanicalFastener without a body, connecting its truss to its wall, and each hanger as an IfcMechanicalFastener HANGER that connects no element; and no longer writes a given member of a wall, roof or floor it lays out. `checkFolder` holds `battens` and a roof opening's `header` to their schemas and warns of a rule, a truss layout, a model's own jack or flat-top family or a joist rule that cannot be laid out as it stands (`framing-layout-rule`, `framing-layout-roof`, `framing-layout-joists`).
- **Against the reference** (`test/fixtures/framing-layout.json`, from `tools/framing-layout-reference.mjs`): a probe of 22 walls and 715 members, every id, name, section, class, outline, length, box and volume exactly, but one top plate whose volume the reference counts with a cap twice. On the reference implementation's garage, guesthouse and villa (29 October 2026, its `main` `f2bc53d2`), all 856 members exactly, and the same GlobalIds, classes, names and tags in the print. Truss layouts and joists (`test/fixtures/framing-roofs.json`, from `tools/framing-roofs-reference.mjs`): a probe of five roofs and two floors, 745 members of walls, roofs and floors, 43 trusses and 82 anchor pairs, exactly; on its garage, second version, and its villa's frame, all 360 and 821 framing members, 11 and 38 trusses and 22 and 65 anchor pairs, with the same GlobalIds, classes, names and tags in the print, but for nine top plates of the villa frame's gables, whose volume the reference counts with a cap twice. Hip ends (`test/fixtures/framing-hips.json`, from `tools/framing-hips-reference.mjs`, none of the reference's examples having one): a probe of nine hipped roofs, 272 trusses, 2049 members of roofs and walls and 533 anchor pairs and hangers, exactly, with the same GlobalIds, classes, names, tags and object types in the print. A truss, a ridge beam, a ridge post and an anchor pair are named in English (`Truss T3`, `Hip truss H1 (ply 2)`, `Jack truss J3`, `Truss T1 (girder, along the ridge)`, `ridge beam`, `ridge post`, `Truss clip pair`), as the steward decided on 30 October 2026; the reference's fixtures hold its Swedish words, and the tests compare names with them put into the rules' English one for one (`test/support/reference-names.mjs`).

**A layer's grain and an element's main direction**, [Part II, 5.1.1](https://openeik.org/spec/0.2/language#5.1.1), of 0.2 (in 0.1 the proposal `grain`), as open question 90 decided on 28 October 2026: a layer of a slab, wall, roof, ceiling or foundation type may carry `grain`, `"along"` or `"across"` the element's main direction; a slab's, a ceiling's or a foundation's main direction runs square to its panels' `direction`, or is its `mainDirection` where it has no panels; a wall's is `"vertical"` (the default) or `"horizontal"`; a roof's is each face's fall line, or its `mainDirection` projected onto the face. It adds two members and no file.

```js
checkModel(model, { proposals: ["grain"] });          // the members held to schemas/proposed/grain/, and main-direction
mainDirectionOf(element);                             // degrees in plan (0 up to 180), "vertical" or "horizontal", or undefined
mainDirectionOf(roof, { edgeId: "roof-a:edge:1" });   // a roof's face: its mainDirection, else that edge's fall line
```

- **Without it**, both are members a schema of 0.1 does not know: kept, and warned of (`member-unknown`).
- **With it**, a layer's `grain` and an element's `mainDirection` are held to their schemas, a value of the wrong kind a `schema-invalid` error at its place; a slab's, a ceiling's or a foundation's `mainDirection` more than 0.01° off square to its panels' `direction` (modulo 180), and `mainDirection` on any other category but a roof's or a wall's, are errors (`main-direction`). The grain changes no body: the compiler and the bridge read neither member.

**The reference implementation's members that 0.2 takes**, [Part II, 14](https://openeik.org/spec/0.2/language#14), with Part II, 4, rule 4 and 7, Part IV, 4.6, rules 12 to 14, and Part I, 11.5, of 0.2 (in 0.1 the proposal `adopted`), as open question 115 decided on 30 October 2026: an element's `castWith` and a slab edge element's `notches`; a layer's `drawing`, a terminal type's `tagSize` and the cut pattern `clt`; a view's `display.electrical`, `display.levelLabels`, `display.sideMarks`, `assembly.pitch`, `lineWeights` and `camera`; a detail item's `cloud`, `schedule.headings`, `legend.title`, `buildUp`, `follows`, `cutBy` and `notDimension`; a detail family's `insertion`; and the reference of a bolt group's `bolts.through`. It adds these members and no file. Their schemas, `schemas/0.2/adopted/`, are generated from the reference implementation's, as 0.1's are, and the proposal's index names them (`adoptedMembers` finds them in a record).

```js
checkModel(model, { proposals: ["adopted"] });     // the members held to their schemas, clt taken, the references followed, the rules checked
compileModel(model, { proposals: ["adopted"] });   // a slab edge element's notches cut, its host's chamfer fill along its whole stretches
ifcOfModel(model, { proposals: ["adopted"] });     // the notched bodies printed
applyCommands(model, [move], { proposals: ["adopted"] });  // copy.move moves and turns the 2D items that follow the instance
unprefixedMembers(view, "views");                  // the reference implementation's own members without their prefix (Part I, 11.5)
```

- **Without it**, the members are members a schema of 0.1 does not know: kept, and warned of (`member-unknown`); `clt` is a value 0.1's schemas refuse (`schema-invalid`); a bolt group's `through` is followed by no check; the compiler builds a notched slab edge element whole, and notes its notches as reference behaviour (`not-derived`).
- **With it**, each member is held to its schema in a folder and in the single-file form; `clt` is taken in a material's and a layer's `pattern`; a member on a category it does not belong to is an error (`adopted-category`: `notches` on any element but a slab edge element), and on a view or a detail item of a kind it means nothing on a warning (`adopted-kind`), but `notDimension` on an item that is no text, an error (`not-dimension`); a bolt group's `through` that names no element is an error (`ref-missing`), and a `castWith`, a layer's family, a build-up's type, a `follows` or a `cutBy` that names nothing a warning (`ref-dangling`); and the rules a schema cannot state are checked: `notch-invalid`, `cast-with`, `layer-drawing`, `camera-points`, `follows-free`, `build-up-type`, `insertion-parameter`. The reference implementation's own members of Part I, 11.5's table, without their prefix, are each a `member-unprefixed` warning that names the extension member it stands for, in place of a `member-unknown`.
- **The compiler** cuts a slab edge element's notches (Part IV, 4.6, rules 12 to 14): its outside upstand and its facing stretch by stretch, its chamfer on its whole stretches alone, and its host's chamfer fill along them; notches rule 12 does not allow are not cut (`edge-notches`). The bridge prints the bodies the compiler gives.
- **The command processor**: `copy.move` moves and turns the plans' free 2D items that follow the instance, and `copy.remove`, `copy.unique` and `original.dissolve` let the items that followed what went follow nothing, and take it out of the regions it cut (Part II, 14.4, rules 6 and 7).
- **Against the reference** (`test/fixtures/notched-edges.json`, from `tools/notches-reference.mjs`): a probe of four notched slab edge elements, an L and a U with chamfers, mitred and square ends, a facing and a flipped element, and their host, every body's volume and box exactly.

**A framing family's parts**, [Part IV, 6.6](https://openeik.org/spec/0.2/compiler#6.6), of 0.2 (in 0.1 the proposal `framing`), as open question 83 decided on 26 October 2026 and amended on 29 October 2026, with no stand-in: a beam's or a slab edge element's framing family (Part II, 5.4) evaluated with the element's parameters, and each part that exists outlined in the family's plane, cut by its cuts, its tail and the parts it is joined to, and carried across the plane by its width. It adds no file and no member.

```js
compileModel(model, { proposals: ["framing"] });  // each family's parts' bodies, by part (and member), and the element's volume
ifcOfModel(model, { proposals: ["framing"] });    // a truss's members as IfcMember, IfcBeam, IfcColumn or IfcPlate under its IfcBeam
checkModel(model, { proposals: ["framing"] });    // framing-expression and framing-part: a family that cannot be evaluated or built
```

- **Without it**, a framing family's body is reference behaviour, as in 0.1: the compiler derives none (`framing-family`, `edge-family`) and the bridge leaves the element out, or writes the bodies an application gives. Nothing else changes: on the reference implementation's seven examples and this repository's, every print and every compiled result is the same, byte for byte, with the other proposals or without them.
- **With it**, the compiler gives each piece of each part that exists a closed body (`polyhedron`, or `mesh` where its outline is not convex) named by its part's id (`part`), and the element a `volume` quantity (`IV.6.6.12`). Where the family has a `member` part or a plate that exists, each part is a member of the element, its bodies tagged `member` as the derived layer tags a framing member (Part IV, 1.1.1): `{id: "<element id>:<part id>", class, predefined, objectType?, name, section}`, its class by its role. A family that cannot be evaluated (`framing-expression`) or built (`framing-part`) gives no body, and an error says why; nothing stands in for it. A beam's joins, connection and holes do not cut its parts (`not-derived`). The bridge writes a family of profiles as the element's own body, each part an IfcShapeAspect with a material constituent of its own; and a family with members as its element's product (an IfcBeam, or a slab edge element's IfcBuildingElementProxy) without a body of its own, its parts under it (IfcRelAggregates), each placed in its placement and keyed `framing:<element id>:<part id>`, typed by class, predefined type and section, a nail plate of steel. `checkFolder` and `checkModel` report, as errors, a family that cannot be evaluated or built, from the records alone.
- **Against the reference** (`test/fixtures/framing-bodies.json`, from `tools/framing-reference.mjs`): a probe of every feature of the language, each part's volume and box, exactly; and the reference implementation's own bodies of its villa's 117 imported rafters (129 parts) and its guesthouse's 78 trusses and rafters (326 parts), exactly, part by part, in count, name, volume and box.
- **Its reference line** ([Part IV, 6.5.1](https://openeik.org/spec/0.2/compiler#6.5.1), open question 10): with the proposal, a family's beam's `reference` in `lines` runs from `(P0, z0 + height/2)` to `(P1, z0 + endRise + height/2)`, the middle of its height over its set-out line, and its `axis` stays `null`: it has no single axis, an analysis taking each `member` part as a member of its own. Without it, both are `null`.

**Reading IFC**, [Part VIII, 2](https://openeik.org/spec/0.2/bridges#2), of 0.2 (in 0.1 the proposal `ifc-read`), as open question 95 decided on 29 October 2026: an element whose shape is the body its reader gave, `shape: "given"`, and the predefined type and the user-defined type an object was read with, `ifcSource.predefinedType` and `objectType`. It adds those members and no file; `eikOfIfc` writes them ([Reading IFC](#reading-ifc)).

```js
const { model, bodies, report } = await eikOfIfc(text);              // records, given bodies and the fidelity report
checkModel(model, { proposals: ["ifc-read"] });                       // the members held to schemas/proposed/ifc-read/
compileModel(model, { proposals: ["ifc-read"] });                     // no body from a given shape's members (given-shape)
ifcOfModel(model, { bodies, proposals: ["ifc-read"] });               // the given bodies, as the classes they were read with
```

- **Without it**, `shape` and the two members of `ifcSource` are members a schema of 0.1 does not know: kept, and warned of (`member-unknown`); and the compiler and the bridge derive bodies from the members that place the element, which are no shape.
- **With it**, they are held to their schemas; an element whose shape is given is held to its category's schema and its references, and to none of the rules that place a body or size one (a door's fit in its wall among them); the compiler derives no body from it and cuts nothing with it (`given-shape`, info); and the bridge prints its given body, as the class and the predefined type it was read with, where the class is one it writes, and no opening for it.
**The site**, [Part II, 2.2](https://openeik.org/spec/0.2/language#2.2), of 0.2 (in 0.1 the proposal `site`), as open questions 44 and 79 decided on 22 and 26 October 2026: a project's `site` may give `trueNorth` alone, which way north is without where the site lies; its `latitude`, `longitude` and `timeZone` are its location, given together or not at all, and with `trueNorth`; and with its location it may name its coordinate reference systems by their EPSG codes, `crs`, `{horizontal, vertical?}` (`"EPSG:3006"`, `"EPSG:5613"`), which the bridge projects into ([Part VIII, 1.7](https://openeik.org/spec/0.2/bridges#1.7)). It adds one member and no file.

```js
checkModel(model, { proposals: ["site"] });   // the project's site held to schemas/proposed/site/site.schema.json
ifcOfModel(model, { proposals: ["site"] });   // an IfcMapConversion into the system site.crs names, where the bridge projects it
siteCrs(site);                                // {horizontal: 3006, vertical: "EPSG:5613"}, or undefined
```

- **Without it**, a site gives all four members, as 0.1's project schema asks: a site of `trueNorth` alone is a `schema-invalid` error at `/project/site`, and `crs` a member the schema does not know (`member-unknown`), which the bridge does not read.
- **With it**, the validator holds a project's site to the proposal's schema in place of 0.1's (`project/project.json` and the single-file form alike): `{trueNorth}`, or all four; part of the location, or the location without `trueNorth`, is a `schema-invalid` error at `/project/site`, and so is a `crs` without the latitude and the longitude or with a code not of the form `EPSG:<number>`. The bridge writes true north in its context, and no latitude or longitude where the site gives none, with the proposal or without it; with it, where its caller names no system (`crs`, which goes first), it writes the IfcMapConversion the site's `crs` names, for the systems it projects (`KNOWN_CRS`), and for any other says in its report that it writes none.
- **The command processor** holds the site of `project.site`, and its `expectedSite`, to the schema the validator holds the project's site to: with it `{trueNorth}` or all four, and `crs` known; without it, 0.1's four members.

**A hosted railing's profile**, [Part II, 12.2.1](https://openeik.org/spec/0.2/language#12.2.1), of 0.2 (in 0.1 the proposal `railings`), as open question 18 decided on 21 and 24 October 2026: the line of a railing on the outline of a slab or a shaft, on a wall's top or along a slab edge element is derived, and its `profile`, the writer's copy 0.1 asks for, is deprecated and may be left out; a railing on a stair keeps its copy. It adds no file and no member.

```js
checkModel(model, { proposals: ["railings"] });   // a hosted railing on no stair without a profile; railing-profile where it has one
```

- **Without it**, every railing carries a `profile` (0.1's element schema), and a hosted railing's is its writer's copy (Part II, 12.2, rule 4).
- **With it**, a railing that names a host and no `railing.side` need not carry a profile; the profile of one on an outline, a wall's top or a slab edge element is a warning (`railing-profile`). The compiler and the bridge derive such a railing's line from its host with the proposal or without it, and read no profile for it. The command processor writes none: such a railing's level and offset follow its host, a profile a command sends is left out with a warning, and a railing whose host's outline changes is not written again.

**Provisions for voids**, [Part II, 6.10](https://openeik.org/spec/0.2/language#6.10) and [Part VIII, 1.10](https://openeik.org/spec/0.2/bridges#1.10), of 0.2 (in 0.1 the proposal `provision`), as the steward decided on 30 October 2026 (open questions, section 4, D9): a void that a run asks of an element it crosses, an element of the category `provision` that carries `provision`, its host, its frame (`axis`, `up`), its `depth`, whether it is `round`, the runs it is for and its host's owner's `answer`, `cut` with what the cut made or `declined` with a sentence. It adds a category and its member, and no file.

```js
checkModel(model, { proposals: ["provision"] });   // provision-frame, provision-host, provision-run, provision-cut
compileModel(model, { proposals: ["provision"] }); // each provision's body, the void asked for
ifcOfModel(model, { proposals: ["provision"] });   // an IfcBuildingElementProxy PROVISIONFORVOID with its property set
```

- **Without it**, a provision is an element of a category 0.1 does not know: kept, checked against the common schema only, with a warning (`category-unknown`), and given no body and no IFC object.
- **With it**, a provision and its type are held to `schemas/0.2/provision/`, and `checkFolder` and `checkModel` report a frame that is no frame (`provision-frame`), a host that is no wall, slab, foundation, ceiling, roof, beam or column (`provision-host`), a run that is no pipe, duct or cable carrier (`provision-run`), and a cut that names a recess its host does not have or an element that is not in the model (`provision-cut`); a host or a run that is not there is a reference that does not resolve (`ref-dangling`). The compiler gives each provision its body (`provisionBody`): a box of its width along across, `up` × `axis`, by its height along `up`, or a rod of its width, its depth along its axis, centred on its point at its level plus its offset; it cuts nothing and has no quantities, and a clash check leaves it out. The bridge writes it as an IfcBuildingElementProxy whose type carries PROVISIONFORVOID, with IFC4's `Pset_BuildingElementProxyProvisionForVoid` (`Shape`, `Width` and `Height` or `Diameter`, `Depth`, `System`), and no opening. In the owners' layout (Part III, 4.4) a provision is its asker's: the owner of the run its `forIds` names first, else `setup`. The command processor (`applyCommands` with a model of 0.2, or of 0.1 with `{ proposals: ["provision"] }`) makes and changes provisions and their types (Part II, 6.10, rule 8), refuses a host or a run of another category (`provision-host`, `provision-run`), and keeps a provision whose host is deleted, with the whole model's warning.

**Declared geometry's nominal sizes, and a stair kit's and a prefabricated wall's frame**, [Part II, 12.1.1](https://openeik.org/spec/0.2/language#12.1.1), of 0.2 (in 0.1 the proposal `declared`), as open question 17 decided on 21 October 2026: the sizes a type sets stay its nominal sizes, whatever its declared envelope says; a stair's type's declaration is placed at the stair's start, x up its first run, and a wall's type's at its baseline's first point, x along it, each y to the left and z up from the base. It adds no file and no member.

```js
checkModel(model, { proposals: ["declared"] });     // declared-size where a type's width or height is off its envelope
compileModel(model, { proposals: ["declared"] });   // a stair kit's and a prefabricated wall's envelope placed on its elements
```

- **Without it**, a declaration on a stair's or a wall's type is kept, not placed (`declared-frame`), and nothing is said of a type's sizes against its envelope.
- **With it**, a type that sets its elements' sizes (Part II, 5.3) and declares an envelope is warned of where its `width` or `height` differs from the envelope's extent along x or z by more than the tolerance for `envelope`, its own or the default (`declared-size`); a stair's and a wall's type's declaration is placed in its frame, its envelope standing for the element's bodies, and its volume the space it takes.

**Components' families deprecated at 0.2**, [Part II, 5](https://openeik.org/spec/0.2/language#5), of 0.2 (in 0.1 the proposal `components`), as open question 73 decided: the families `toilet` and `basin` are deprecated, a terminal of the kind `wc` or `washbasin` standing for each. With the proposal, `checkFolder` and `checkModel` warn about each such component type (`component-retired`). The frames and bodies question 73 makes class-A rules are not written yet (open questions, 99).

**A component's frame and its bodies**, [Part IV, 7.6](https://openeik.org/spec/0.2/compiler#7.6), of 0.2 (in 0.1 with the same proposal), as open questions 73 and 99 decided: every component's frame, its `axes` whatever its family, else its profile in plan, flipped to the right, a plate on a column on its centre at its top, a cast-in item at each item of its row on its wall by `insertAt`; and its bodies that are boxes and prisms. With the proposal, `compileModel` gives a timber section its box, a plate its outline less its holes (a level prism, or convex pieces stood on its edge or in its own axes; its holes' circles in 16 pieces as the reference draws them, its volume exact), a weld its fillet, a bolt group each bolt's shank, a cylinder of its diameter from z = 0 to its `grip` (a rod in axes of its own), and a cast-in item its block or its bar at each item, each element its `volume` (`IV.7.6`); it says what stays reference behaviour (`not-derived`: a bolt's head, washers and nut, the holes bolts drill, a bracket's or a hanger's legs, a lattice tie, a conduit, a recess, a void form), and gives a component that has no frame no body (`component-frame`), which `checkFolder` and `checkModel` warn of too. `ifcOfModel` writes plates, bolt groups, welds and the others by rule, by their classes, and a given body over a bolt group or a drilled plate. Against the reference (`test/fixtures/components-bodies.json`, from `tools/components-reference.mjs`): 25 probe components, plates, timber sections, welds and cast-in items exactly in volume and box, each shank across as the reference's and along within it; a timber section in axes of its own differs, as question 99 decided (`componentFrame`, `castInItems`, `componentBodies`, `holeRing` and `HOLE_SIDES` in `src/compile-components.js`, also the package's).

**Classification references**, [Part II, 11](https://openeik.org/spec/0.2/language#11), of 0.2 (in 0.1 the proposal `classifications`), as open questions 4 and 86 decided: any record may carry `classifications`, references `{system, code, edition?, uri?}` to codes of systems OpenEik does not list, at its top (the project in its `project`, a level, any record of a record list). It adds one member and no file.

```js
checkModel(model, { proposals: ["classifications"] });   // each record's classifications held to its schema
ifcOfModel(model, { proposals: ["classifications"] });   // an IfcClassificationReference for each reference (Part VIII, 1.8)
classificationsOf(record);                               // its references of the form rule 2 gives, each once
```

- **Without it**, `classifications` is a member a schema of 0.1 does not know: kept, and warned of (`member-unknown`); the bridge does not read it.
- **With it**, each record's `classifications` is held to `schemas/proposed/classifications/classifications.schema.json`, a breach a `schema-invalid` error at its place (an empty list, a reference without a system or a code, a `uri` that is not absolute, one reference twice); a member a reference does not define is a `member-unknown` warning. No system or code is checked against a list. The bridge writes one IfcClassification per system and edition, one IfcClassificationReference per reference and an IfcRelAssociatesClassification relating it to the products, spaces, IFC types, project and storeys whose records carry it; the references of a record it writes no object for (a view, a grid, an unused type, a material) are named in its report's `leftOut`.

**A member between two points**, [Part II, 6.2.1](https://openeik.org/spec/0.2/language#6.2.1) and [Part IV, 6.7](https://openeik.org/spec/0.2/compiler#6.7), of 0.2 (in 0.1 the proposal `member`), as open question 110 decided on 30 October 2026: the category `member`, in `structure`, for a brace, a strut, a tie or a truss's member, neither a beam nor a column. Its profile's two points, at its `levelId` and `offset` and at its `end`'s (`{levelId, offset}`), are the ends of its reference line, the line through the middle of its section; they may be one point in plan, so that it stands plumb. It takes a beam's section, `roll`, `taper` and `joins`.

```js
checkModel(model, { proposals: ["member"] });     // held to schemas/0.2/member/; member-length, member-plumb
compileModel(model, { proposals: ["member"] });   // its section swept along its line, its line and axis in result.lines
ifcOfModel(model, { proposals: ["member"] });     // an IfcMember, NOTDEFINED, its section an IfcMaterialProfileSet on its type
memberLine(element, (levelId) => elevation);      // its ends, its length, its direction and whether it is plumb
memberFrame(line, roll);                          // its section's x and v, before and after its roll
```

- **Without it**, `member` is a category this package does not know: kept, checked against the common schema, given no body, and left out of a print.
- **With it**, a member's element and type are held to their schemas and filed in `structure`; its two ends lie 10 mm apart or more (`member-length`), its `end.levelId` names a level (`ref-missing`), and plan points under 1 mm apart but not one point are a warning (`member-plumb`). The compiler sweeps its section along its line, centred on it: `x` the left normal of its plan direction, or the model's x where it is plumb, `v = t × x`, both turned right-handed about the line by its `roll`; a taper is centred on the line; its joins, its axis, its volume and its length are a beam's. A beam and a column are read as before. Extension members, as an analysis application's own, are kept on it as on any record. The command processor (`applyCommands` with a model of 0.2, or of 0.1 with `{ proposals: ["member"] }`) makes and changes members and their types (Part II, 6.2.1, rule 9): it refuses an `end` on no level (`ref-missing`) and a height other than its section's depth (`type-size`), and a `level.delete` with `moveTo` moves an end on the level, keeping its height.

**A slab on a sloped plane**, [Part II, 6.3.1](https://openeik.org/spec/0.2/language#6.3.1) and [Part IV, 4.2.1](https://openeik.org/spec/0.2/compiler#4.2.1), of 0.2 (in 0.1 the proposal `slopedSlab`), as open question 111 decided on 30 October 2026: a slab may carry `plane`, the plane of its top, as `slope` (degrees) or `rise` over `run`, rising from an edge `edgeId` into its outline, or falling along `fall` (degrees in plan) from its profile's first point, at its level plus its offset. It adds one member and no file.

```js
checkModel(model, { proposals: ["slopedSlab"] });     // plane held to its schema; slab-plane, slab-plane-edge
compileModel(model, { proposals: ["slopedSlab"] });   // each layer between the plane lowered by its depths square to it
slabPlane(slab, elevation + offset);                  // its top as z0 + gx·x + gy·y, its slope, its fall and k
```

- **Without it**, `plane` is a member a schema of 0.1 does not know: kept, warned of, and the slab is level.
- **With it**, `plane` is held to `schemas/0.2/slopedSlab/plane.schema.json`; `plane` on another category, an `edgeId` that names no edge, and a `rise` over `run` over 75° are errors (`slab-plane`), and so is a slab edge element on a slab whose plane slopes (`slab-plane-edge`). The compiler builds each layer as a prism of the outline between the plane lowered by its depths measured square to it, its edges plumb and its voids cut plumb, with a `slopedArea` beside its `area` in plan; an attached column base or wall top, a railing on its outline and, with the proposal `rooms`, a room's ceiling read the plane; with `framingLayout` it has no joists and bears no framed wall. The bridge writes it as an IfcSlab whose layers' bodies lie between the planes.

**A room's boundary, ceiling and quantities**, [Part IV, 7.5](https://openeik.org/spec/0.2/compiler#7.5), of 0.2 (in 0.1 the proposal `rooms`), as open question 72 decided on 26 October 2026: a room placed by a point is bounded by the face of its level's plan that holds its point, its walls joined as Part IV, 7.4.2 joins them and a roof that comes down through its floor among what bounds it, its holes the walls and columns that stand free in it; its ceiling is its own height, the undersides of the slabs, ceilings and roofs over it, the next storey, or its element's height; and it measures what IFC's `Qto_SpaceBaseQuantities` names. It adds no file and no member.

```js
compileModel(model, { proposals: ["rooms"] });   // result.rooms: each room's boundary, ceiling, edges, openings and volume; its quantities
ifcOfModel(model, { proposals: ["rooms"] });     // an IfcSpace for every room with a boundary, its volume its body, its eight base quantities
checkModel(model, { proposals: ["rooms"] });     // rooms-enclosed, rooms-shared
```

- **Without it**, room boundaries are reference behaviour, as in 0.1: the compiler finds none, and the bridge writes an `IfcSpace` only for a room drawn by hand, its outline up to its height, which its fidelity report calls approximated where the height is the element's. Nothing else changes: on the reference implementation's examples and this repository's, every print and every compiled result is the same, byte for byte, with the other proposals or without them.
- **With it**, the compiler's result has `rooms`, one entry per room with a boundary (`compile-rooms.js`): its `boundary` (outer ring counter-clockwise, holes clockwise); its `ceiling`, where it comes from (`room`, `found`, `storey` or `height`) and its planes, a roof's over its extent (`over`) and the plane beyond the roofs (`beyond`); its `edges`, what each stretch of its boundary lies against (`wall`, `column`, `separator` or `open`, and the element); the doors and windows in its walls (`openings`); and its `volume`, prisms from its floor up to its ceiling. Its quantities are IFC's names, camel-cased: `height`, `grossPerimeter`, `netPerimeter`, `netFloorArea`, `grossWallArea`, `netWallArea`, `netCeilingArea` and `netVolume` (`IV.7.5.6`, `IV.7.5.7`). A room placed by a point in no closed face has no boundary (`rooms-enclosed`), and one in the face of a room whose id comes first in code-unit order neither (`rooms-shared`); a room under a barrel vault has its boundary, area and perimeter, and its ceiling is not derived (`not-derived`). The bridge writes an `IfcSpace` for every room with a boundary and a ceiling, its body its volume, with its eight base quantities; a room without one is left out, and the report says why; a room imported as an IfcSpace keeps its GlobalId and its imported property sets (VIII.1.5). `checkFolder` and `checkModel` warn of the same two (`rooms-enclosed`, `rooms-shared`).
- **Against the reference** (`test/fixtures/rooms.json`, from `tools/rooms-reference.mjs`): a probe of 14 rooms (a free-standing column, a room separator, a room on its line, two rooms in one face, an open room, a 20 mm joint, a wall that bounds no room and a high one, a room round the outer corner of two joined walls, a corner whose wall stops 125 mm short, a gable wall drawn by its outline, a gable roof over a room, a room drawn by hand beyond its eaves, a loft under a roof that comes down through its floor, the project's `openingMinArea`), and the 25 rooms of the reference's one example with rooms, each kept to what rooms read. 32 rooms agree exactly: boundary, area, perimeter, ceiling, height, walls, skirting, openings, ceiling area and volume. The other seven lie beside a room separator or a floor's edge, which 7.5 draws as a line and the reference as a strip 0.2 mm wide: each is larger by 0.1 mm along it, which the test checks to 10⁻⁷ m², and with strips in place of the lines the example's rooms agree to 10⁻⁹ m².

## Diagnostic codes

The rule is the specification's: `I.11.2.8` is Part I, 11.2, rule 8, and `SP.7.3.1` the security and privacy considerations, 7.3, rule 1.

| Code | Severity | Rule | What |
|---|---|---|---|
| `eik-missing` | error | I.9.2.5 | the folder has no `eik.json` |
| `eik-format-missing` | error | I.9.2.4 | `eik.json` has no `format` |
| `format-unsupported` | error | I.9.2.3 | `eik.json`, or a single-file model's `format`, names a format this package does not implement (it implements `openeik/0.1` and `openeik/0.2`, any pre-release); or a write would take a folder of 0.2 back to 0.1 |
| `schema-version-deprecated` | info | I.9.3 | a model of 0.2 gives `schemaVersion`, which 0.2 deprecates and does not read: its format is its version; a writer of 0.2 adds none, and a migrated model keeps its own until 1.0 removes it |
| `eik-assumed` | info | VI.2.2 | a version without `eik.json`, read as `openeik/0.1-draft` because its project has `schemaVersion` 1 |
| `file-missing` | error | III.12 | `project/project.json` or `project/levels.json` is missing |
| `json-invalid` | error | III.12.3 | a record file is not JSON |
| `json-too-deep` | error | III.12.3 | a file nests deeper than 256 levels of objects and arrays, the most this tool reads; in the single-file form, each file the model is written as (the project file, the levels, each record, each view and its chunks of detail items), so that what is written reads back and what reads is written. A record nests about ten, and the limit keeps every walk of the package far from the end of its stack. Refused, as a file that is not JSON is, naming the file and the member under which it nests too deep (`pointer`), never a crash: by the check, the reader, `versionTree`, `filesOfTree`, the writer, the command processor and the merge |
| `number-not-finite` | error | I.3.5 | a number that is not finite: `1e999` is valid JSON text, and JSON.parse reads it as Infinity. Refused where it is (`pointer`), as `NaN` is |
| `string-not-unicode` | error | SP.7.3.1 | a string or a member's name holds a lone surrogate: `"\ud800"` is valid JSON text, and JSON.parse reads it as half of a pair, which no UTF-8 text holds, nor the canonical form (RFC 8785; Part III, 6.1), so no record that holds one has a hash. Refused where it is (`pointer`), as a file that is not JSON is, as Security and privacy, 7.3, rule 1 asks a reader (a SHOULD: no rule of Parts I to III refuses it outright): by the check, the reader, `readModel`, `versionTree`, `filesOfTree` and the diff |
| `number-not-double` | error | SP.7.3.1 | a number that expresses more magnitude or precision than a double provides (RFC 7493, 2.2), by the test of Security and privacy, 7.3: beyond ±(2⁵³ − 1), where no reader can be expected to hold it exactly (`9007199254740993`, `1e16`; a value, as a check of a model given as one and the writer see it), or neither the double it reads as, rounded to as many significant digits as it is written with (either way at a tie), nor, however spelled, that double's shortest form (Part III, 5, rule 5): more precise than a double (`3.141592653589793238462643383279`, which reads as 3.141592653589793115997963468544 to its 31 digits) or nearer 0 than any double but 0 (`1E-400`, read as 0). `0.1`, `1.0`, `1e2` and `-0` pass, and so do `0.10000000000000001`, as C's `%.17g` writes 0.1, a double's exact value, however many digits it has, and the shortest form of 2⁻⁴⁴, `5.684341886080802e-14`, though that double rounded to its 16 digits is 5.684341886080801e-14. A number is named as it is written. `scripts/number-test.mjs` holds the scan to the test, computed exactly in Python, on some 270 000 literals. Refused where it is (`pointer`), as a file that is not JSON is: by the check, the reader, `readModel`, `versionTree`, `filesOfTree` and the diff; the writer writes no such value |
| `member-repeated` | error | SP.7.3.1 | an object of a model's file names a member twice, its escapes read (`"a"` and `"\u0061"`; RFC 7493, 2.3): JSON.parse reads the last and other readers the first, so two readers would read two records. Refused where it is (`pointer`), as a file that is not JSON is, by the same readers. Security and privacy, 7.3, rule 1 asks a reader to refuse it |
| `utf8-invalid` | error | III.12 | a model's file is not UTF-8: no text, which no reader can read, so every reader refuses it, with or without the whole-model check (Part III, 12; the stored form is UTF-8, 5, rule 1): a record or `eik.json` in a folder, a single-file model (`checkModel`, `readModel`), a store's answer, or a kept file's object of a version that holds a lone surrogate. Refused, naming the file, and never read with U+FFFD in place of what is no text. `AGENTS.md`, no file of the model, is read as it is; a kept file that is not UTF-8 is left alone, an info (kept-skipped) |
| `category-unknown` | warning | I.10.5 | an element or type of a category this tool does not know: kept, and checked against the common schema only |
| `schema-invalid` | error | III.3.2 | a record does not validate against its schema; `pointer` says where |
| `member-unknown` | warning | I.10 | a member the schema does not know: one that no subschema applying to its object names, as `unevaluatedProperties: false` would read `"x-eik-closed": true` there (Part I, 10, rule 3). Kept, never refused |
| `application-extension` | warning | I.11.2.8 | an extension member at the top of a result manifest or a proposal (the prefix in `data.prefix`), where the application's own data goes in `engine`, `summary` and `evidence` (Part VI, 8): kept |
| `member-extension` | info | I.11.2 | an extension member, `<prefix>:<name>`, that an application adds to a record, or to an object of a result manifest or a proposal whose members its schema names (`app`, `made`, `base.records[…]`) (the prefix in `data.prefix`): kept, and not called a misspelling. A name with a reserved prefix (`eik:`, `eik-tools:`, `project:` …) or one not of a prefix's form is a `member-unknown` |
| `extension-null` | warning | I.11.2.3 | an extension member whose value is `null`, which no application writes of its own, since in a patch `null` takes a member away (Part V, 4.2): kept, as every member a reader reads is (Part I, 10, rule 1). The command processor refuses a command that would write one into a record that did not hold it |
| `eik-extension` | warning | I.11.2.8 | `eik.json` carries a member this version does not define, an extension member or another: no application adds a member to it, and only a later version of the specification does (Part III, 8.1); kept |
| `order-unknown` | warning | I.11.2.8 | a member of `project/order.json` that names no record list, an extension member or another: no application adds one. The file is an index of the record lists, not a record, so this package's reader and writer do not keep the member (Part III, 9, rule 5) |
| `stored-form` | error | III.5 | a file is not in the stored form. `readFolder` reads the file all the same and gives the finding as a warning: a reader reads any I-JSON layout of a record (Part III, 5, rule 6) |
| `path-id`, `path-level`, `path-category`, `path-view-kind` | error | III.4.2 | a file's name or folder disagrees with its record: a name made from an id that reads (Part III, 2, rule 7) as another id, as `wall%41.json` (`wallA`) for the record `wall%41` |
| `path-unescaped` | warning | III.2.9 | a record's file name, its level folder or its view's folder reads as its id or `levelId`, but is not the path segment Part III, 2 makes of it: a character the rules escape left as it is, as in a folder written before 14 October 2026 (`study-roof:gutter:study-roof:edge:0.json` for `study-roof%3Agutter%3Astudy-roof%3Aedge%3A0.json`), or an escape they do not make (`%3a`, `%41`). The record is read, the message names the path the rules give, and `writeFolder` moves the file there |
| `path-discipline` | error | III.4.1 | a record is in a folder that neither layout gives it, 0.1's (III.4.1) nor its owner's (III.4.4); in 0.1 without the proposal ownership, in another discipline folder than its category's |
| `path-fold` | error | I.6.7 | two records whose paths fold to one, compared name by name by their folds (Part III, 4.3: each name in Normalization Form D, its case folded by the full case folding of Unicode 13.0.0, then in Normalization Form C; canonical caseless matches): two files (`Wall-A.json` and `wall-a.json`, `straße.json` and `strasse.json`, `café` in two normalisations) or two of their folders; or two levels whose folders do (`Level-1` and `level-1`), whether an element stands on them or not. Where names fold (macOS, Windows) the two are one file or folder, and the two are not both in a model (Part I, 6, rule 7). The later path in code-unit order, or the later level, is named, with the other; a folder that two spellings make one is named once, on the first file in it. The writer refuses such a model (`path-collision`, with `path-fold` diagnostics), comparing the paths as the validator does. Two levels whose ids fold to one but whose folders do not (`con` and `Con`, which make `%63on` and `%43on`) are no error |
| `id-duplicate` | error | I.6.1 | two records of one list share an id |
| `id-slash` | error | I.6.4 | an id contains `/`; in a model of 0.3, or of 0.2 with the proposal `groupIds`, an original's or a copy's id contains `/`, or a stored id begins with a copy's id and a `/` where that copy is not its original's `frame` (I.6.1.2) |
| `id-derived` | error | II.10.1.4 | in a model of 0.3, or of 0.2 with the proposal `groupIds`: a stored element has an id an instance of a group derives |
| `group-frame` | error | II.10.1.1 | in a model of 0.3, or of 0.2 with the proposal `groupIds`: an original's `frame` names no copy of it, or one that does not stand where the original says its kept elements stand, or a group's first instance has a copy record while its elements are the kept ones |
| `group-id` | error | II.10.1.2 | in a model of 0.3, or of 0.2 with the proposal `groupIds`: a kept element drawn in another instance than the kept one has an id that does not begin with the `frame` and a `/` |
| `id-too-long` | error | I.6.8 | an id whose path segment (Part III, 2) is longer than 200 bytes of UTF-8, escapes counted as written (`:` is `%3A`, three bytes): a level's, a view's, or the id of a record kept in a file of its own, so that `<segment>.json` stays well inside the 255 bytes a file system takes for a name. The writer refuses it (`model-invalid`, with an `id-too-long` diagnostic) |
| `path-unsafe` | error | III.2 | the writer's refusal of a record whose path would not stay a file of the model's folder: a category or a view's kind, which are not escaped, that makes a path segment `.` or `..`, a path that would lead out of the folder where `\` separates folders, as on Windows (a category of a later version `a\..\..\..\..\x`), or a path whose reading there holds a name `.` or `..`, a hidden one (`.claude`, `.vscode`, `.git`, `.eik`) or a reserved one (`~`), or is `eik.json`; an id or a level never does, escaped as Part III, 2 says. It names the record; the validator does not report it, since a category may be one a later version adds. Also `insidePath`'s refusal of a path on disk that leaves the folder or goes through a link |
| `ref-missing` | error | I.7 | a reference that must resolve (a type, a level, a top constraint's level, an opening's, a slab edge's, a gutter's or a railing's host, a wall's attached top, a column's base, a member's join) names nothing |
| `ref-dangling` | warning | I.7.3 | a reference that should resolve names nothing |
| `type-category` | error | I.7.1 | an element's type is of another category |
| `host-category` | error | II.6.1 | an opening's host is no wall; a slab edge's host is not a slab or foundation, a rebar's not a slab, a foundation, a beam, a column or a slab edge, a mesh's not a slab, a foundation or a wall; a gutter's is no roof; a railing's is not a stair, a slab or a shaft (II.12.2) |
| `cost-code-member` | error | II.11.1.3 | in a model of 0.3, or of 0.2 with the proposal `estimate`: a cost code gives an `angle` with no `item` `bend`, an `insulation` with no `item` `insulation`, or `includes` with an `item` |
| `profile-short` | error | II.6.1.2 | a wall's or beam's two profile points are less than 10 mm apart in plan |
| `room-number` | error | II.6.1.4 | two rooms on one level share a number |
| `edge-ids` | error | II.6.1.10 | an outline's edge ids are not unique, or not one per point |
| `bridge-kind-twice` | error | II.2.1 | two thermal bridges of the project's envelope have the same `kind`, so a tool would not know which ψ to take |
| `lambda-missing` | warning | II.4.3 | a layer of a part of the thermal envelope (a type an element uses that has an insulation layer), other than a membrane or an air layer, with no design thermal conductivity: neither its own `thermal.lambda` nor its material's `lambda` |
| `frames-fit` | warning | IV.4.3.1 | in 0.2, or in 0.1 with the proposal frames, a door or window whose type's family leaves no room for its frame, a light's sash or glass, or a leaf's glass, or whose glass is thicker than what holds it: a compiler derives it no body of its own |
| `main-direction` | error | II.5.1.1 | in 0.2, or in 0.1 with the proposal grain, a slab's, ceiling's or foundation's `mainDirection` that is not square to its panels' `direction` within 0.01° (rule 2), or `mainDirection` on an element of a category other than slab, ceiling, foundation, roof or wall (rule 5) |
| `member-length` | error | II.6.2.1.3 | in 0.2, or in 0.1 with the proposal member, a member whose two ends, its profile's points at its level plus its offset and at its end's, lie less than 10 mm apart |
| `member-plumb` | warning | II.6.2.1.4 | in 0.2, or in 0.1 with the proposal member, a member whose two plan points lie less than 1 mm apart but not within 10⁻⁶ mm: its section turns with the direction from one to the other; a plumb member's are one point |
| `slab-plane` | error | II.6.3.1.6 | in 0.2, or in 0.1 with the proposal slopedSlab, `plane` on an element other than a slab, or a slab's plane whose `edgeId` names no edge of its outline, or whose `rise` over `run` is steeper than 75° |
| `slab-plane-edge` | error | IV.4.2.1.6 | in 0.2, or in 0.1 with the proposal slopedSlab, a slab edge element hosted by a slab whose plane slopes, which hosts none in 0.2 |
| `declared-size` | warning | II.12.1.1.1 | in 0.2, or in 0.1 with the proposal declared, a type that sets its elements' sizes declares an envelope whose extent along its frame's x or z differs from its `width` or `height` by more than the declaration's tolerance; the sizes stay nominal |
| `leaf-material` | warning | IV.4.3.1.14 | in 0.2, or in 0.1 with the proposal frames, a window's or a garage door's family gives `leafMaterial`: only a door has a leaf of its own |
| `frames-profile` | error | IV.4.3.1.15 | in 0.2, or in 0.1 with the proposal frames, a family's `frameProfile`, `mullionProfile` or `sashProfile` is no outline of Part II, 5.2 (rule 9), its bounding box is not the width by the depth of the members it shapes, or it shapes a frame that follows its wall |
| `derived-path` | warning | IV.1.1.2.1 | a file in a folder's derived layer at no result's path: a result lies at `.eik/derived/elements/<id>.json` or `.eik/derived/types/<id>.json`, its id written as a path segment; a reader ignores it (by `readDerived`) |
| `derived-form` | warning | IV.1.1.2.2 | a result of a folder's derived layer that is not JSON, breaks its schema (`derived/bodies.schema.json`) or does not name its own record among its sources: a reader discards it (by `readDerived`) |
| `derived-stale` | info | IV.1.1.2.4 | a result of a folder's derived layer whose sources no longer hold, by their hashes, or of a record the model does not have, or, for a reader that wants its own rules' result, of another producer or rule: a reader discards it, and derives it again or does without (by `readDerived`) |
| `provision-frame` | error | II.6.10.1 | in 0.2, or in 0.1 with the proposal provision, a provision whose `axis` or `up` is no unit vector, or whose `up` is not square to its `axis` |
| `provision-host` | error | II.6.10.3 | in 0.2, or in 0.1 with the proposal provision, a provision whose host is no wall, slab, foundation, ceiling, roof, beam or column |
| `provision-run` | error | II.6.10.4 | in 0.2, or in 0.1 with the proposal provision, a provision for an element that is no pipe, duct or cable carrier; in 0.4, or in 0.3 with the proposal provisionTerminals (II.6.10.1), no terminal and no component on a pipe or a duct either |
| `provision-cut` | warning | II.6.10.5 | in 0.2, or in 0.1 with the proposal provision, a cut answer that names a recess its host does not have, or an element that is not in the model |
| `masonry-category` | error | II.4.3.1 | in 0.4, or in 0.3 with the proposal masonry, a material that carries masonry and whose category is not masonry, brick, block or stone, or that has none |
| `railing-profile` | warning | II.12.2.1.4 | in 0.2, or in 0.1 with the proposal railings, a railing on the outline of a slab or a shaft, on a wall's top or along a slab edge element carries a `profile`, which is deprecated: its line is derived |
| `component-frame` | warning | IV.7.6 | in 0.2, or in 0.1 with the proposal components, a component that has no frame: axes whose x is zero or whose y runs along it, a profile whose two points are one, a plate's column without a level, a centre or a height, or a cast-in item whose `hostId` names no wall with a straight baseline: a compiler derives it no body |
| `adopted-category` | error | I.10.4 | in 0.2, or in 0.1 with the proposal adopted, a member it adds on an element or a type of a category it does not belong to: `notches` on any element but a slab edge element (Part IV, 4.6, rule 12) |
| `adopted-kind` | warning | II.14 | in 0.2, or in 0.1 with the proposal adopted, a member it adds on a view or a detail item of a kind it means nothing on: `display.electrical` on a view that is no plan, `camera` on one that is not 3D, `cloud` on a detail item that is no line, `buildUp` on one that is no leader, `cutBy` on one that is no region; kept, and not read |
| `notch-invalid` | error | IV.4.6.12 | in 0.2, or in 0.1 with the proposal adopted, a slab edge element's `notches` that rule 12 does not allow: on a section without an outside upstand, shorter than 10 mm, ending past its edge, below the flange, shallower than the chamfer, or overlapping |
| `cast-with` | warning | II.14.1.1 | in 0.2, or in 0.1 with the proposal adopted, an element whose `castWith` names itself |
| `layer-drawing` | warning | II.14.2.1 | in 0.2, or in 0.1 with the proposal adopted, a layer's `drawing` that names a family's type and no family, or a type its family does not have |
| `camera-points` | error | II.14.3.6 | in 0.2, or in 0.1 with the proposal adopted, a view's `camera` whose position and target are less than 1 mm apart |
| `follows-free` | warning | II.14.4.5 | in 0.2, or in 0.1 with the proposal adopted, a detail item that follows an instance of a group and is tied to an element or a view, or is not in a plan: its `follows` is not read |
| `build-up-type` | warning | II.14.4.4 | in 0.2, or in 0.1 with the proposal adopted, a build-up text whose `typeId` names a type without layers |
| `not-dimension` | error | II.14.4.8 | in 0.2, or in 0.1 with the proposal adopted, `notDimension` on a detail item that is not a text |
| `insertion-parameter` | error | II.14.5.1 | in 0.2, or in 0.1 with the proposal adopted, a detail family's `insertion` bound to a parameter the family does not have |
| `member-unprefixed` | warning | I.11.5.2 | in 0.2, or in 0.1 with the proposal adopted, a member of the reference implementation's own written without its prefix (`calloutShape` for `stomra:calloutShape`, and the others of Part I, 11.5); kept, a member 0.2 does not know |
| `component-retired` | warning | II.5 | in 0.2, or in 0.1 with the proposal components, a component type of the family `toilet` or `basin`, deprecated at 0.2: a terminal of the kind `wc` or `washbasin` stands for it |
| `stairs-layout` | warning | IV.7.3.1 | in 0.2, or in 0.1 with the proposal stairs, a straight, L or U stair that cannot be laid out (no risers or going, its top not above its base, a first run of as many risers as it has, an L's landing shallower than its run is wide): a compiler gives it no layout and no body |
| `framing-expression` | error | IV.6.6.1 | in 0.2, or in 0.1 with the proposal framing, a beam or a slab edge element whose type's framing family does not evaluate (an expression that is invalid, a parameter outside its range or named twice, or one the element gives that is not of scope instance): a compiler derives it no body |
| `framing-part` | error | IV.6.6.1 | in 0.2, or in 0.1 with the proposal framing, a beam or a slab edge element whose type's framing family's parts cannot be built (a Length, a width, a depth or a plate's thickness outside its range, a member under 1 mm long, an outline that is not simple, a cut through a part's centre, a joint by a part that is no part or is not convex, a tail without its parameters, a part cut away, a plate off its members or on a slab edge element): a compiler derives it no body |
| `framing-layout-roof` | warning | IV.7.4.13 | in 0.2, or in 0.1 with the proposal framingLayout, a roof whose truss layout's family type is no beam type whose framing is a truss family, one that declares the instance parameters Pitch, HeelHeight, OverhangLeft and OverhangRight: a compiler does not lay it out; or whose model's own jack or flat-top family (its family type's id and -jack or -flat) is no truss family, or a flat-top family declares no FlatLength of the scope instance: a compiler leaves out each hip truss, jack or girder along the ridge built of it (IV.7.4.16.2) |
| `framing-layout-joists` | warning | IV.7.4.15 | in 0.2, or in 0.1 with the proposal framingLayout, a layer's joist rule that cannot be laid out as it stands (on a type that is not a slab type, on a layer of no thickness, or with a joist deeper than its layer): a compiler lays it out as Part IV, 7.4.15, rule 1 says, or not at all |
| `framing-layout-rule` | warning | IV.7.4.1 | in 0.2, or in 0.1 with the proposal framingLayout, a framed layer whose rule cannot be laid out as it stands (a stud, a plate, a header's plies or a sill deeper than the layer, a spacing less than the stud's width, tiers out of order, a relyOn that names no other framed layer, a rule on a type that is not a wall's, a layer with no thickness): a compiler lays it out as Part IV, 7.4.1, rule 3 says, or not at all |
| `rooms-enclosed` | warning | IV.7.5.3 | in 0.2, or in 0.1 with the proposal rooms, a room placed by a point that lies in no closed face of the walls, columns and room separators on its level: a compiler gives it no boundary |
| `rooms-shared` | warning | IV.7.5.4 | in 0.2, or in 0.1 with the proposal rooms, a room placed by a point in the face of another room whose id comes first in code-unit order: that room keeps the face, and a compiler gives this one no boundary |
| `north-missing` | warning | II.6.8.5 | the project gives no `site.trueNorth`, and the model holds a door, a window or a roof window: their bearings take plan +Y as north |
| `roof-window-type` | error | II.6.3 | a roof window's `typeId` (`roof.windows[]`) names a type of a category other than `window` |
| `library-unused` | warning | III.13.2 | a type or material no record uses (a writer should not write library records the model does not use) |
| `details-orphan` | warning | III.10 | detail chunks without their `view.json` |
| `kept-collision` | error | III.7.4 | a kept file sits where a record is, or where a record needs a folder |
| `kept-path` | error | III.7.1 | a kept file of the single-file form has a path that is not plain: a name that starts with `.` or `~`, a `\` or NUL, or the path `eik.json` or `AGENTS.md`. Such a file in a folder is left alone, with an info. A name that begins as a Windows drive does (`C:/notes.md`) is plain, and holds a `:`, which Part III, 7, rule 8 keeps out of kept paths (below) |
| `kept-skipped` | info | III.7.2 | a binary file, one over 1 MiB, or one with a path that is not plain: left alone, not part of the model |
| `kept-name` | warning | III.7.8 | a file at a plain path that holds a name a file system refuses or reads otherwise, which Part III, 2 would escape (its rules 2 to 5): a character a common file system refuses (`: * ? " < > \|` and control characters; so a name that begins as a Windows drive does, `C:`), a trailing dot or space, or a device name (`con.txt`, `Aux`, `lpt¹.txt`). A `%` is none (rule 1 escapes it only so that an escape reads back as one), so an application's folder named by its ids' segments (`acme/frame%3Aa/`) holds kept files. A kept path is not escaped, so the file is no kept file: the reader leaves it alone, not part of the model, never removed and in no version, and warns. In the single-file form an error at its entry of `extras`, which the model holds as it was given and no writer writes (the writer refuses it, `kept-name`) |
| `kept-fold` | warning | III.7.8 | a kept file whose path folds to one, as `path-fold` compares paths, with a record's path, `eik.json`'s or `AGENTS.md`'s, whether or not the folder has them, or the path of a kept file that comes before it in code-unit order: `Eik.json`, `agents.md`, `Types/notes.md` beside the type folders, `notes/readme.md` beside `notes/README.md`; or one that passes through `eik.json` or `AGENTS.md` as through a folder (`AGENTS.md/notes.md`), each a file at the top of the folder. The reader leaves it alone, not part of the model, never removed and in no version, and warns; in the single-file form an error at its entry of `extras`, which the model holds as it was given and no writer writes (the writer refuses it, `kept-fold`). Two such kept files that a merge brings together, one from each side, are a conflict, `in the way`, on the later |
| `name-reserved` | warning | III.1.2 | a name starting with `~` is reserved; the file is not read |
| `eik-unsafe` | error | III.1.1 | the hidden folder `.eik` is a link or no plain folder, or `.eik/remotes.json` or its temporary file is a link or no plain file: nothing is staged, written or read through it. A folder may arrive (in an archive, a git clone) with `.eik` a link that leads anywhere; `writeFolder` stages its files in `.eik/`, and the remotes keep their tracking refs there, so each refuses it before anything is written, read or sent |
| `tree-name` | error | VI.2 | a version tree's entry name is no path segment that stays in its folder: empty, `.` or `..`, or holding `/`, `\` or NUL; or no name a folder gives: a hidden one (`.git`), which a reader of a folder skips, one that starts with `~` and is no shard (a sub-tree named `~` and two lowercase hexadecimal digits, in a folder's own tree whose entries are all shards; a shard holds no shard), or one in a shard its name's hash does not give it (Security and privacy, 5.3, rule 2). A name with a `:` is one, as the file of a record written before Part III, 2 escaped it may hold (its rule 9); a kept file's with one is no kept file's (below). A reader of versions (`listLeaves`, `folderEntries`, `filesOfTree`, `readVersion`, `clone`, `pull`, the diff) refuses the tree, naming it by its hash and the entry by its pointer (`/tree/<n>/0`), and `parseTree`, which reads one tree object alone, refuses a name that is no path segment; `pushVersion` refuses a folder whose version would hold such a name (a record's file named with a `\` as it is, by hand or by a writer before Part III, 2 escaped it, which Linux and macOS keep; the writer names it `%5C`), so that every version written reads back. A version that holds such a name stays unread, as Security and privacy, 5.3, rule 2 asks |
| `tree-duplicate` | error | VI.2 | two entries of one folder of a version tree have the same name, in one tree or in two of its shards: refused, naming the tree that holds the second |
| `tree-kept` | error | III.7.8 | a version holds a kept file that a reader of a folder leaves alone or reads as another file: at a path no kept file may have (Part III, 7, rule 8), a name a file system refuses or reads otherwise (`C:/notes.md`, `notes/con.txt`), a path that folds to one with another of the version's (`notes/a.md` beside `Notes/b.md`, `EIK.json`, `agents.md`) or one through `eik.json` or `AGENTS.md`; a text over 1 MiB, or holding NUL (rule 2); or a leaf read as a kept file (a JSON string, or an object at no record's place) at a path no kept file has (rules 1 and 4): `AGENTS.md`, which a reader of a folder reads as no file of the model, or a record's file, `<leaf>.json` where `<leaf>` is a record's place (`project/materials/notes.json`, `drawings/views/plan/v1/view.json`), which it reads as that record. No folder gives such a version, and a reader that left the file out, as it leaves one out of a folder, would drop it from the next version it stores; so `filesOfTree`, and through it `readVersion`, `clone`, `pull` and a merge of versions, refuse the version, leniently or not, naming each such leaf (Security and privacy, 5.3, rule 2); `readVersion` with `repair` leaves each out and gives it back (rule 4). A `%` is no such character: `notes/50%.md` is a kept file |
| `outline-points` | error | II.6.1.3 | a foundation's profile is neither an outline of three points or more nor, for a round footing type, one point (the other categories' point counts are the schemas') |
| `outline-gap` | error | II.6.1.3 | two consecutive points of a profile's outline, the last and the first included, are less than 0.1 mm apart (Part II states the rule for profiles, not for the outlines in `voids`) |
| `outline-area` | error | II.6.1.3 | a profile's outline has an area of less than 100 mm² |
| `outline-crossing` | error | II.6.1.3 | edges of a profile's outline cross or touch; neighbouring edges meet only at their shared point, so one folding back over the other counts. Within 10⁻⁶ mm (10⁻¹² of the coordinates beyond 10⁶ mm), so that decimal coordinates on a line or an edge are on it, whatever binary rounding does |
| `component-points` | error | II.6.1.8 | a component's two profile points, its insertion point and its orientation, are one point (a cast-in item placed by `insertAt` excepted) |
| `opening-fit` | error | II.6.1.5 | a door or window reaches past its wall's length, has its bottom below the wall's base, or its top over the wall's height (from the wall's top constraint, Part IV, 5.1) where the wall's attached top does not rise over it (5.3). Under an attached top Part IV does not derive (a barrel vault; a host, its type or a layer missing, which their own codes report) the height is not judged |
| `opening-overlap` | error | II.6.1.5 | two doors or windows of one wall overlap in its elevation: by their boxes, or by their outlines where either one's type has one, as Part IV, 4.3 cuts them (reported once, on the one whose id sorts later); openings that only touch do not |
| `top-below-base` | error | IV.5.1 | a wall's, shaft's or stair's top constraint does not lie above its base |
| `type-size` | error | II.5.3 | an element's `width` or `height` differs from the size its type sets (a door or window of a family; a component, unless it is a contour plate, bolt group or weld, carrying `plate`, `bolts` or `weld`; a round footing; a cast-in item), or a sectioned beam's `height` from its section's depth |
| `footing-diameter` | error | II.5 | a round footing type's `width` differs from its diameter |
| `section-size` | error | II.5.2 | a section's `web` is not less than its `width`, or twice its `flange` not less than its `depth`; an `L`'s or a `T`'s `flange` not less than its `depth`; a `2L`'s `gap` not less than its `width`, its `web` not less than `(width − gap) / 2`, or its `flange` not less than its `depth`; a `round` section's `width` and `depth` not equal; a `rectangle`'s `radius` more than half its smaller side; a `custom` section's `width` or `depth` more than 0.1 mm from its outline's bounding box |
| `section-outline` | error | II.5.2.9 | a `custom` section's outline breaks rule 9: a contour of fewer than three entries or two corners, two `through` entries together, entries less than 0.1 mm apart, a `through` point on its chord, a corner radius where the contour does not turn, beside an arc or too large for its sides, a contour that crosses or touches itself, a void outside the outline or meeting it or another void, or inside another |
| `layer-thickness` | error | II.5.1.2 | a layer other than a membrane has thickness 0, or a type's layers sum to 0 |
| `layer-id` | error | II.5.1.3 | two layers of one type share an id |
| `layer-member` | error | II.5.1.5 | a layer has an `inset` in a type other than a slab or roof type, or an `extent` in a type other than a roof type |
| `base-host` | error | IV.5.2.1 | a column's attached base is on something other than a foundation or slab, or its centre is outside the host's footprint (on its boundary counts) |
| `base-length` | error | IV.5.2.4 | a column's attached base is not below its top: its derived length is not positive |
| `top-host` | error | IV.5.3.1 | a wall's attached top is on something other than a roof, ceiling, slab or foundation, or the host's footprint does not cover the wall's whole centreline. A pitched roof's footprint is its outline and each edge's overhang, mitred at its convex corners (Part IV, 4.4.3); a round footing's, its circle |
| `top-layer` | error | II.6.4 | a wall's attached top names a layer (`{layerId}`) its host's type does not have |
| `top-face` | error | IV.5.3.6 | the face a wall's top is attached to does not rise above the wall's base anywhere along its centreline, or falls below it by more than the host's own depth there; or, in the level mode, the level top (the face's lowest over the core band, 5.3.5) is not above the base |
| `trim-edge` | error | II.6.7.1 | an outline edge trimmed to a wall's face (`edgeAttachments`) is no edge of the outline (its `edgeIds`, else `<id>:edge:<n>`), is trimmed a second time, or is a round footing's, which has no edges |
| `trim-host` | error | II.6.7.2 | an outline edge is trimmed to the face of an element that is not a wall (a host not in the model is `ref-missing`) |
| `trim-parallel` | error | II.6.7.3 | an outline edge trimmed to a wall's face turns more than 5° from the wall's baseline, either way along it |
| `trim-face` | error | II.6.7.4 | an end of an outline edge trimmed to a wall's face lies more than 1 mm beyond the face, on the side away from the wall: a trim cuts an edge back to the face and never moves it out. The face is the baseline moved by half the wall's thickness, its type's layers or else its `width` (Part IV, 4.5, rule 1) |
| `trim-area` | error | II.6.7.5 | the outline trimmed at every face its edges are trimmed to keeps less than 100 mm²; judged where every entry's face is known |
| `beam-rise` | error | IV.6.2.2 | a beam with neither a section nor a framing family has a non-zero `endRise` |
| `join-mitre` | error | IV.6.4.3 | a mitre join's `to` is not a beam or column, or the join has `extend: false` |
| `join-duplicate` | error | IV.6.4.5 | two joins of one member share an id |
| `join-self` | error | IV.6.4.5 | a member is joined to itself |
| `declared-load` | error | II.12.1.4 | a load of a declaration names no fixing the declaration declares (an element's declaration that declares no fixings: none its type's declares) |
| `declared-fixing` | error | II.12.1.8 | a fixing's `direction` is not a unit vector (its length more than 0.001 from 1) |
| `declared-body` | error | II.12.1.4 | a declared body is no body of its kind: a prism's or cylinder's `top` not above its `bottom`, a rod from a point to itself, a face naming a vertex the body does not have, a `polyhedron` that is not convex, a `mesh` that is not closed |
| `declared-basis` | warning | II.12.1.5 | a record a declaration's `basis` names has changed since (its hash differs) or is not in the model: the declaration is kept, and unconfirmed |
| `declared-faces` | warning | II.6.6.6 | a record's declared geometry holds 10 000 faces or more (each face of a polyhedron or a mesh, n + 2 for a prism of n points, 3 for a cylinder or a rod) |
| `railing-type` | error | II.12.2.1 | a railing's type has no `railing` payload |
| `railing-host` | error | II.12.2.2 | a railing on a stair names no `side`, one on a slab's or a shaft's outline no `edges`, or one on a wall's top or along a slab edge element a `side`, `edges` or `insets` |
| `railing-edges` | error | II.12.2.3 | a railing names an edge its host does not have (its `edgeIds`, else `<host id>:edge:<n>`) |
| `railing-run` | error | II.12.2.2 | a railing's edges do not follow one another round its host's outline as one run (naming every edge is a closed run) |
| `railing-posts` | error | II.12.2 | a railing's `posts.at` is not in increasing order |
| `railing-post-layout` | warning | II.12.2 | a given post stands past the railing's line's end, or two stand farther apart in plan than its type's `postSpacing` (by more than 0.5 mm): reported, not refused. The line of a railing on an outline is its run moved by its insets (Part IV, 11.2); with no host, its profile; on a stair it is not measured |
| `generic-points` | error | II.6.1.12 | a generic element's two profile points are one point |
| `generic-shape` | error | II.6.6.2 | a generic element has no declared geometry, its own or its type's |
| `edge-section` | error | II.5.6.1 | a slab edge type's L or U section whose flange is as deep as its depth, or whose web (a U's twice) is as wide as its width: it has no inside |
| `edge-chamfer` | error | II.5.6.2 | a slab edge type's chamfer on a rectangle, wider than its upstand's web, or higher than its depth less its flange |
| `gutter-section` | error | II.5.6.3 | a gutter type's half-round section without its diameter, or its box section without its width or its depth |
| `edge-host` | error | II.6.1.6 | a slab edge element whose `hostEdgeId` is missing or names no edge of its host's outline |
| `gutter-host` | error | II.6.1.13 | a gutter without a `hostId`, or whose `hostEdgeId` is missing or names no edge of its roof's outline (a host that is no roof: `host-category`) |
| `edge-thickening` | error | IV.4.6.11 | a slab or a foundation whose thickened edges do not fit it: at a depth a beam reaches, the edges moved in by the beams' widths close no simple outline inside it, or a void reaches into them |
| `opening-hinge` | warning | II.6.5.6 | a door's or a side-hinged garage door's family that hinges at its `top` or its `bottom`: its operation is not defined |
| `opening-operation` | warning | II.6.5.6 | an opening family of another kind than `door` whose `operation` is `slide` or `pocket`: not read |
| `insulation-bti` | warning | II.6.9.4 | a pipe type's `pipe.bti` or a pipe's `mep.insulation.bti`, which is deprecated: the warning names the code and the thickness to write instead |
| `insulation-member` | warning | II.6.9.3 | a run's own `mep.insulation` gives `none` and more, a duct's a `bti`, or its own `code` without its own `thickness` |
| `component-host` | error | II.6.9.6 | a terminal whose type mounts on a pipe (a component) does not name a pipe of the model in `hostId`, or one whose type mounts on a duct names something other than a duct of the model there (a duct part with no `hostId` is valid; a dangling one is also `ref-missing`) |
| `component-along` | warning | II.6.9.7 | a component's `along` is past the end of its pipe or duct, by more than 1 mm; it stands at the run's end |
| `component-place` | warning | II.6.9.8 | a component's `levelId`, `profile` or `offset` is not where its pipe or duct and `along` put it, within 1 mm and 1°, or a component in a duct's `width` or `height` is not its size across, within 1 mm |
| `component-ports` | warning | II.6.9.9 | a type that mounts on a pipe or a duct has `connections` or `outlets` of its own |
| `room-tag` | error | II.13.6.4 | a tag of the field `room` does not name a room of the model |
| `connect-symbol` | error | II.13.6.7 | a line's end is joined (`connect`) to a detail item that is no symbol of the line's view |
| `application-invalid` | warning | VI.8 | a kept file whose `format` is a result manifest's or a proposal's (by its id or an earlier one) does not validate against its schema: kept, and not read as one; the model stays valid |
| `application-format` | warning | VI.8 | a kept file names an `openeik-result/…` or `openeik-proposal/…` format this tool does not implement: kept, and not read as one |
| `quality-likely-railing` | warning | II.6.2 | with `--quality`: an element of another category looks like a railing |
| `quality-likely-window` | warning | II.6.2 | with `--quality`: an element of another category looks like a window |
| `quality-likely-door` | warning | II.6.2 | with `--quality`: an element of another category looks like a door |
| `quality-likely-stair` | warning | II.6.2 | with `--quality`: an element of another category looks like a stair, or a tread of one |
| `quality-likely-beam` | warning | II.6.2 | with `--quality`: an element of another category looks like a beam |
| `electrical-profile` | error | XIII.2.3.1 | in 0.2, or in 0.1 with the proposal electrical ([Proposals](#proposals)): an electrical element's profile is not exactly two points, `at` and `toward`, at least 1 mm apart in plan |
| `electrical-size` | error | XIII.2.3.3 | in 0.2, or in 0.1 with the proposal electrical: an electrical element's `width` or `height` differs from its type's, which sets them; a linear device's (a track, or a light on a ceiling or a floor at least three times as wide as it is deep) `height` differs from its type's, or its `width`, its own length, is not from 1 to 50 000 mm |
| `controls-kind` | error | XIII.2.3.4 | in 0.2, or in 0.1 with the proposal electrical: an element names what it controls (`electrical.controls`), and its type's kind is not `switch`, `dimmer`, `button` or `sensor` |
| `controls-missing` | error | XIII.2.3.4 | in 0.2, or in 0.1 with the proposal electrical: an id of `electrical.controls` names no element of category `electrical` |
| `controls-self` | error | XIII.2.3.4 | in 0.2, or in 0.1 with the proposal electrical: an element names itself in `electrical.controls` |
| `fire-emergency-light` | warning | XIII.2.3.7 | in 0.2, or in 0.1 with the proposal electrical, a `fire` type whose `fire.kind` is `emergencyLight`, deprecated at 0.2: an emergency luminaire is an `electrical` element of the kind `emergencyLight` |
| `light-kind` | error | XIII.2.3.6 | in 0.2, or in 0.1 with the proposal electrical: a type gives a luminaire's light output (`luminousFlux`, `colorTemperature`, `beamAngle`, `distribution`), and its kind is not `light`, `spot`, `emergencyLight` or `track` |
| `circuit-missing` | error | XIII.3.3.1 | in 0.2, or in 0.1 with the proposal electrical: an element's `electrical.circuitId` names no circuit of the model |
| `circuit-board` | error | XIII.3.3.2 | in 0.2, or in 0.1 with the proposal electrical: a circuit's `boardId` names no element, or one that is not an electrical element of kind `board` or `meterCabinet` (`null` is the supply) |
| `circuit-designation-twice` | warning | XIII.3.3.3 | in 0.2, or in 0.1 with the proposal electrical: two circuits of one board have the same designation, or two supplies, which count as one board's circuits. The later by id is named, with the other |
| `board-cycle` | error | XIII.3.3.4 | in 0.2, or in 0.1 with the proposal electrical: following a board's feed, board by board (its `circuitId`, that circuit's `boardId`), comes back to it; each board on the loop is named |
| `feed-missing` | error | XIII.4.1.1 | in 0.2, or in 0.1 with the proposal electrical: `electrical.from` names no element of category `electrical` |
| `feed-other-circuit` | error | XIII.4.1.1 | in 0.2, or in 0.1 with the proposal electrical: a device that has `from` has no `circuitId`, or another than the device its `from` names |
| `feed-cycle` | error | XIII.4.1.2 | in 0.2, or in 0.1 with the proposal electrical: following `from`, device by device, comes back to where it started; each device on the loop is named |
| `lighting-view` | error | XIII.4.3.1 | in 0.2, or in 0.1 with the proposal electrical: a view gives a lighting scene (`display.lighting`), and its kind is not `3d` |
| `lighting-kind` | error | XIII.4.3.2 | in 0.2, or in 0.1 with the proposal electrical: an id of a lighting scene's `on` names an element that is no luminaire, an electrical element of the kind `light`, `spot`, `emergencyLight` or `track` |
| `lighting-dim` | error | XIII.4.3.3 | in 0.2, or in 0.1 with the proposal electrical: a key of a lighting scene's `dim` is not on: not in its `on`, or, where `on` is `"all"`, an element that is no luminaire |
| `lighting-missing` | warning | XIII.4.3.2 | in 0.2, or in 0.1 with the proposal electrical: an id of a lighting scene's `on` names no element of the model, a luminaire deleted since the scene was made; and, where `on` is `"all"`, a key of its `dim` that names none (XIII.4.3.4, the rule the finding names) |
| `run-invalid` | error | XIII.5.1 | in 0.2, or in 0.1 with the proposal electrical: a cable carrier or a modelled cable is no run as Part IV, 12.1 says of a pipe (no `mep`, not one height of `mep.z` for each point, fewer than two points, or all at one place) |
| `cable-to` | error | XIII.6.1 | in 0.2, or in 0.1 with the proposal electrical: a modelled cable's `cable.to` names no electrical element, or one whose `circuitId` is not `cable.circuitId` |
| `cable-carrier` | error | XIII.6.2 | in 0.2, or in 0.1 with the proposal electrical: an id of `cable.carrierIds` names no element of category `cableCarrier` |
| `cable-ends` | warning | XIII.6.4 | in 0.2, or in 0.1 with the proposal electrical: no piece of a modelled leg starts within 500 mm of its upstream device or board, or none ends within 500 mm of its `to`, measured in 3D from each point at `elev(levelId) + offset` or its height; a leg of a supply only at its end. Named on the leg's first piece by id, at `/cable/to` |
| `sheet-application-record` | warning | XIV.2.1.1 | in a model of 0.2, a sheet that does not give every member Part XIV, 2.1 requires (`number`, `name`, `revision`, `width`, `height` and `viewports`): a sheet of 0.1 kept as an application record, which no drawing is issued on; the migration to 0.2 leaves it so and names it (Part I, 9.8, rule 5) |
| `sheet-number-twice` | error | XIV.2.4.1 | in 0.2, or in 0.1 with the proposal drawings ([Proposals](#proposals)): two sheets have the same number, or numbers whose path segments fold to one: each names a folder of drawings/issued/ |
| `sheet-number-pattern` | warning | XIV.2.4.2 | in 0.2, or in 0.1 with the proposal drawings: a sheet's number does not match the drawing profile's numbering |
| `sheet-viewport-view` | error | XIV.2.4.3 | in 0.2, or in 0.1 with the proposal drawings: a sheet's viewport places a view that is not in the model |
| `sheet-viewport-twice` | error | XIV.2.4.3 | in 0.2, or in 0.1 with the proposal drawings: two viewports of a sheet have the same id |
| `sheet-detail-number-twice` | warning | XIV.2.4.4 | in 0.2, or in 0.1 with the proposal drawings: two viewports of a sheet have the same detail number |
| `sheet-title-block` | warning | XIV.2.4.5 | in 0.2, or in 0.1 with the proposal drawings: a sheet's titleBlockId, not none, names no title block of the model |
| `sheet-renumbered` | error | XIV.2.4.7 | in 0.2, or in 0.1 with the proposal drawings: a sheet that has an issued drawing now carries another number, and the drawing's register does not end in withdrawn |
| `detail-ref-unresolved` | warning | XIV.3.4.1 | in 0.2, or in 0.1 with the proposal drawings: a reference of a detail item names no record of the model: the item is an orphan, kept |
| `provenance-agent` | error | XIV.3.4.2 | in 0.2, or in 0.1 with the proposal drawings: an item whose provenance.by is agent lacks agent, actingFor, instruction.digest, made.version or status (by schema) |
| `provenance-accepted` | error | XIV.3.4.3 | in 0.2, or in 0.1 with the proposal drawings: an accepted item lacks acceptedBy or acceptedAt (by schema), or is accepted by an agent |
| `issued-with-proposed-item` | error | XIV.3.4.4 | in 0.2, or in 0.1 with the proposal drawings: a sheet was issued while a view placed on it held an item whose provenance.status is proposed, and its detail chunks are unchanged since; a dependent view holds its primary's items, which it draws (XIV.3) |
| `provenance-instruction` | error | XIV.3.4.5 | in 0.2, or in 0.1 with the proposal drawings: provenance.instruction holds more than digest and ref: never the instruction's text (by schema) |
| `profile-schema` | error | XIV.4.3.1 | in 0.2, or in 0.1 with the proposal drawings: the drawing profile does not validate against its schema, or its numbering is no regular expression |
| `profile-code-twice` | error | XIV.4.3.2 | in 0.2, or in 0.1 with the proposal drawings: a code is twice in the drawing profile's statuses, purposes, decisions or reviewStates, or a family twice in its fonts |
| `profile-missing` | error | XIV.4.3.3 | in 0.2, or in 0.1 with the proposal drawings: the folder holds an issued drawing and no drawings/profile.json |
| `profile-font-missing` | warning | XIV.4.3.4 | in 0.2, or in 0.1 with the proposal drawings: a font of the drawing profile is not in the folder at assets/<sha256>.<ext>: no one else can draw the sheet again |
| `profile-fonts` | error | XIV.4.3.5 | in 0.2, or in 0.1 with the proposal drawings: the folder holds a linked issued drawing, and the drawing profile gives no `fonts`; a profile that only gives a model's reviews their codes needs none |
| `person-schema` | error | XIV.5.2.1 | in 0.2, or in 0.1 with the proposal drawings: a person's record does not validate against its schema |
| `person-unknown` | error | XIV.5.2.2 | in 0.2, or in 0.1 with the proposal drawings: a member of the part's files that names a person names no record of project/people/ |
| `person-email` | error | XIV.5.2.3 | in 0.2, or in 0.1 with the proposal drawings: a member that names a person, or a person's id, holds an email address |
| `key-twice` | error | XIV.5.2.4 | in 0.2, or in 0.1 with the proposal drawings: a keyid is used twice in the registry |
| `key-window` | error | XIV.5.2.4 | in 0.2, or in 0.1 with the proposal drawings: a key's validUntil is before its validFrom |
| `key-removed` | error | XIV.5.2.5 | in 0.2, or in 0.1 with the proposal drawings: a key that a sign-off names was removed, not ended with validUntil (needs the history); not checked by `checkFolder` |
| `svg-bytes` | error | XIV.6.12.1 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG is not UTF-8 without a byte-order mark, or holds CR or NUL (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-markup` | error | XIV.6.12.2 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG holds an XML declaration, DOCTYPE, entity declaration, comment, processing instruction or CDATA section (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-not-canonical` | error | XIV.6.12.3 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG is not its own Canonical XML 1.1 form followed by one LF (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-lines` | warning | XIV.6.12.4 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG is not written one element per line (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-root` | error | XIV.6.12.5 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG's root is not svg in the SVG namespace, or another namespace is declared (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-paper` | error | XIV.6.12.6 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG's width, height or viewBox is not the sheet's paper in mm (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-title` | error | XIV.6.12.7 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG's first child is not a title that is not empty (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-element` | error | XIV.6.12.8 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG holds an element the profile does not allow (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-attribute` | error | XIV.6.12.9 | in 0.2, or in 0.1 with the proposal drawings: an element of an issued SVG holds an attribute the profile does not allow it (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-value` | error | XIV.6.12.10 | in 0.2, or in 0.1 with the proposal drawings: a colour, transform, orient or unit of an issued SVG is not as the profile says (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-number` | error | XIV.6.12.11 | in 0.2, or in 0.1 with the proposal drawings: a number of an issued SVG is not written as the profile says (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-reference` | error | XIV.6.12.12 | in 0.2, or in 0.1 with the proposal drawings: an href or url() of an issued SVG names no element of the file of the right kind, or something outside it (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-use` | error | XIV.6.12.13 | in 0.2, or in 0.1 with the proposal drawings: a use of an issued SVG recurses, or the file expanded through its uses holds more than 1 000 000 elements (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-id-twice` | error | XIV.6.12.14 | in 0.2, or in 0.1 with the proposal drawings: an id is used twice in an issued SVG (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-too-large` | error | XIV.6.12.15 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG is over 64 MiB, or holds over 2 000 000 elements or a depth over 32 (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-font` | error | XIV.6.12.16 | in 0.2, or in 0.1 with the proposal drawings: a font-family of an issued SVG names no family of the drawing profile's font set, or the derivation's fonts do not name each family used with the set's digest (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-layers` | error | XIV.6.12.17 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG's root children, viewport groups or layers are not in the order the profile says, with one sheet layer, before the viewport groups or after them, or the viewport groups are not the derivation's (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-order` | error | XIV.6.12.18 | in 0.2, or in 0.1 with the proposal drawings: where generator.order is id, the children of a layer of an issued SVG are not sorted by id (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-derived-unnamed` | error | XIV.6.12.19 | in 0.2, or in 0.1 with the proposal drawings: a drawn primitive of a derived layer, but a 3D view's picture and the generator's annotation groups, has no ancestor-or-self carrying data-eik-id or data-eik-record, or several (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-element-twice` | error | XIV.6.12.20 | in 0.2, or in 0.1 with the proposal drawings: a derived layer of an issued SVG holds two groups of one element (where generator.order is paint, two of one element and part), an element's groups there are not all parts numbered 0, 1, 2, … in the order of the file, or data-eik-part is on anything but a part (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-element-unknown` | error | XIV.6.12.21 | in 0.2, or in 0.1 with the proposal drawings: a data-eik-id of an issued SVG names no element of the model, while the records it read are unchanged (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-hash` | error | XIV.6.12.22 | in 0.2, or in 0.1 with the proposal drawings: an element's group or a shared path of an issued SVG has no data-eik-hash, or one that is not the hash of its canonical bytes (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-annotation` | error | XIV.6.12.23 | in 0.2, or in 0.1 with the proposal drawings: an annotation group of an issued SVG names no item of its viewport's view, or its origin disagrees with its layer (by `checkDrawingSvg`, `src/drawings-svg.js`). A dependent view's items are its primary's, which it draws (XIV.3), checked while they are those of the issue: the derivation names the primary as it is now, or the read set holds its view.json and its chunks as they are now |
| `svg-viewport` | error | XIV.6.12.24 | in 0.2, or in 0.1 with the proposal drawings: data-eik-viewport or data-eik-view of an issued SVG disagree with the derivation and the sheet (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-derivation` | error | XIV.6.12.25 | in 0.2, or in 0.1 with the proposal drawings: an issued SVG holds no metadata openeik-derivation whose text is the derivation in canonical JSON, valid against its schema (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-outside-crop` | warning | XIV.6.12.26 | in 0.2, or in 0.1 with the proposal drawings: an element's group of an issued SVG lies wholly outside its viewport's crop (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `svg-anchor` | error | XIV.6.12.27 | in 0.2, or in 0.1 with the proposal drawings: an annotation's data-eik-anchor is not attached, moved, elementOnly or orphaned (by `checkDrawingSvg`, `src/drawings-svg.js`) |
| `issued-file-unknown` | warning | XIV.7.4.1 | in 0.2, or in 0.1 with the proposal drawings: a drawing's folder holds a file that is none of drawing.svg, revision.json, read-set.json and signoffs/<n>.json |
| `issued-folder-name` | error | XIV.7.4.2 | in 0.2, or in 0.1 with the proposal drawings: a drawing's folder's name, read back, is not its revision record's drawing |
| `drawing-file-schema` | error | XIV.7.4.3 | in 0.2, or in 0.1 with the proposal drawings: a drawing file names no format, or another than the format of its place: it is not read as one |
| `issued-file-changed` | error | XIV.7.4.4 | in 0.2, or in 0.1 with the proposal drawings: an issued drawing's files were changed or removed other than by a later revision (needs the history); not checked by `checkFolder` |
| `asset-digest` | error | XIV.7.4.5 | in 0.1 with the proposal drawings and not the proposal assets: an asset's name is not the SHA-256 of its bytes and one of the extensions of Part XIV, 7.3 of 0.1 (`pdf`, `ttf`, `otf` and `asice`); in 0.2, Part XV, 2 says what an asset is, and reports a file that is none (asset-name) |
| `register-missing` | error | XIV.7.4.6 | in 0.2, or in 0.1 with the proposal drawings: a folder of drawings/issued/ has no register, or a register no folder |
| `revision-schema` | error | XIV.8.5.1 | in 0.2, or in 0.1 with the proposal drawings: a revision record does not validate against its schema |
| `revision-derivation` | error | XIV.8.5.2 | in 0.2, or in 0.1 with the proposal drawings: a revision's drawing, revision or title is not its derivation's, or its drawing not the number of the sheet the derivation names, unchanged since |
| `revision-svg-digest` | error | XIV.8.5.3 | in 0.2, or in 0.1 with the proposal drawings: a revision's svg.sha256 or svg.bytes is not drawing.svg's (an unlinked revision's file.sha256 or file.bytes not its asset's) |
| `revision-derivation-differs` | error | XIV.8.5.4 | in 0.2, or in 0.1 with the proposal drawings: a revision's derivation is not the derivation in its drawing.svg, member for member |
| `revision-read-set` | error | XIV.8.5.5 | in 0.2, or in 0.1 with the proposal drawings: a derivation's readSet.hash is not read-set.json's hash, or its count not its number of distinct pairs |
| `revision-sequence` | error | XIV.8.5.6 | in 0.2, or in 0.1 with the proposal drawings: a revision's sequence is not its superseded revision's plus 1, or supersedes disagrees with the register |
| `revision-status` | error | XIV.8.5.7 | in 0.2, or in 0.1 with the proposal drawings: a revision's status is not of the drawing profile, or its state, sharedFor or purpose disagree with it |
| `revision-code` | warning | XIV.8.5.8 | in 0.2, or in 0.1 with the proposal drawings: a revision's code is not the one the profile's series gives for its status and sequence |
| `revision-date` | warning | XIV.8.5.10 | in 0.2, or in 0.1 with the proposal drawings: a revision's date is earlier than the superseded revision's |
| `revision-by-missing` | warning | XIV.8.5.12 | in 0.2, or in 0.1 with the proposal drawings: a shared, published or as-built revision does not say who issued it (`by`) |
| `unlinked-shape` | error | XIV.8.5.11 | in 0.2, or in 0.1 with the proposal drawings: an unlinked revision has svg or derivation, or no file, or its folder holds drawing.svg or read-set.json |
| `read-set-shape` | error | XIV.9.7.1 | in 0.2, or in 0.1 with the proposal drawings: a read set does not validate, is not of its revision, or a list is not sorted by path, each path once |
| `read-set-viewports` | error | XIV.9.7.2 | in 0.2, or in 0.1 with the proposal drawings: a read set's viewports are not exactly the derivation's, by id and view |
| `read-set-incomplete` | error | XIV.9.7.3 | in 0.2, or in 0.1 with the proposal drawings: a read set lacks the pair of an element drawn, its type, the view or a detail chunk, or the sheet's list one of the sheet, its title block, the project, the levels or the profile (a missing type, or a pair of the sheet's list, names rule XIV.9.7.4) |
| `read-set-volume` | error | XIV.9.7.5 | in 0.2, or in 0.1 with the proposal drawings: a read set's inVolume is not what the volume test held at the source version (needs the history); not checked by `checkFolder` |
| `read-set-self-check` | error | XIV.9.7.6 | in 0.2, or in 0.1 with the proposal drawings: a revision is stale against its own source version: its read set is incomplete (needs the history); not checked by `checkFolder` |
| `cloud-change-unknown` | error | XIV.10.3.1 | in 0.2, or in 0.1 with the proposal drawings: a cloud or mark of the revisions layer names no change item of the revision, or no viewport of the sheet |
| `change-twice` | error | XIV.10.3.2 | in 0.2, or in 0.1 with the proposal drawings: two change items of a revision have the same id |
| `cloud-when-off` | error | XIV.10.3.3 | in 0.2, or in 0.1 with the proposal drawings: a revision whose clouds are off or throughout holds a cloud of its own |
| `cloud-earlier` | warning | XIV.10.3.4 | in 0.2, or in 0.1 with the proposal drawings: the revisions layer holds an earlier revision's cloud, and the profile keeps no earlier clouds |
| `change-unlisted` | warning | XIV.10.3.6 | in 0.2, or in 0.1 with the proposal drawings: an element drawn differently from the superseded revision is in no change item (needs the history); not checked by `checkFolder` |
| `register-schema` | error | XIV.11.3.1 | in 0.2, or in 0.1 with the proposal drawings: a register does not validate against its schema |
| `register-order` | error | XIV.11.3.2 | in 0.2, or in 0.1 with the proposal drawings: a register's issued entries are not in sequence from 1, a rendition names a sequence not issued before it, or an entry follows withdrawn |
| `register-behind` | error | XIV.11.3.3 | in 0.2, or in 0.1 with the proposal drawings: a register's last issued entry is not the drawing's folder: its sequence, record, svg or asset |
| `register-rewritten` | error | XIV.11.3.4 | in 0.2, or in 0.1 with the proposal drawings: a register does not hold every entry of an earlier version's, unchanged and in order (needs the history); not checked by `checkFolder` |
| `register-kind` | error | XIV.11.3.5 | in 0.2, or in 0.1 with the proposal drawings: a register's kind is not its revisions', or an entry does not give what its kind gives |
| `issue-id` | error | XIV.12.2.1 | in 0.2, or in 0.1 with the proposal drawings: an issue does not validate, or its id is not its name read back |
| `issue-revision-unknown` | error | XIV.12.2.2 | in 0.2, or in 0.1 with the proposal drawings: a revision an issue lists is not in its drawing's register with that sequence and record |
| `issue-purpose` | error | XIV.12.2.3 | in 0.2, or in 0.1 with the proposal drawings: an issue's purpose is not a code of the drawing profile's purposes |
| `issue-drawing-twice` | error | XIV.12.2.4 | in 0.2, or in 0.1 with the proposal drawings: an issue lists a drawing twice |
| `review-id` | error | XIV.13.4.1 | in 0.2, or in 0.1 with the proposal drawings: a review does not validate, or its id is not its name read back |
| `review-revision-unknown` | error | XIV.13.4.2 | in 0.2, or in 0.1 with the proposal drawings: a review's drawing, sequence, svg or asset, and record name no revision of the drawing's register |
| `review-code` | error | XIV.13.4.3 | in 0.2, or in 0.1 with the proposal drawings: a review's profile, an item's state or a decision is not the drawing profile's; where the profile lists no `reviewStates` or no `decisions`, no state or decision is |
| `review-anchor-unknown` | warning | XIV.13.4.4 | in 0.2, or in 0.1 with the proposal drawings: a review item's anchor names an element the reviewed revision does not draw, or a viewport not of its sheet |
| `review-after-close` | error | XIV.13.4.5 | in 0.2, or in 0.1 with the proposal drawings: an item, reply, state or decision of a closed review is later than its close |
| `review-carried-from` | warning | XIV.13.4.6 | in 0.2, or in 0.1 with the proposal drawings: an item's carriedFrom names no item of another review of the same drawing the folder holds |
| `review-change-unknown` | warning | XIV.13.4.7 | in 0.2, or in 0.1 with the proposal drawings: a review item's change names no change item of the reviewed revision |
| `review-shape` | error | XIV.13.4.9 | in 0.2, or in 0.1 with the proposal drawings: a review item's shape breaks its `form`: a closed polyline; a polygon, rectangle or ellipse that is not closed; a polygon of fewer than three points; a rectangle or an ellipse that is not the four corners of a rectangle in order, within 0.01 mm |
| `signoff-shape` | error | XIV.14.7.1 | in 0.2, or in 0.1 with the proposal drawings: a sign-off does not validate, or its payload does not decode to an in-toto Statement v1 |
| `signoff-predicate-type` | error | XIV.14.7.2 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's predicateType is not the drawing sign-off's, or its predicate does not validate |
| `signoff-subject` | error | XIV.14.7.3 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's subjects are not the drawing's SVG (or asset) and revision.json with the digests of the register's entry it names |
| `signoff-predicate` | error | XIV.14.7.4 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's predicate's drawing, sequence, revision or status are not those of a revision of the register |
| `signoff-key-unknown` | error | XIV.14.7.5 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's signer names no person of the registry, or a signature's keyid no key of that record |
| `signoff-agent-action` | error | XIV.14.7.6 | in 0.2, or in 0.1 with the proposal drawings: an agent signs another action than checkedByTool, or a person signs checkedByTool |
| `signoff-decision` | error | XIV.14.7.7 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's decision is not a code of the profile's decisions, of which a profile that lists none has none (a warning when a decision action gives none) |
| `signoff-timestamp-missing` | error | XIV.14.7.8 | in 0.2, or in 0.1 with the proposal drawings: a signature of a sign-off has no time-stamp |
| `signoff-signature` | error | XIV.14.7.9 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's signature or time-stamp does not verify, or the key was not valid at the time-stamp's time (by `verifyDrawingSignoffs`, `src/drawings-verify.js`, not `checkFolder`) |
| `signoff-withdraws` | error | XIV.14.7.10 | in 0.2, or in 0.1 with the proposal drawings: a withdrawal names no sign-off of the same revision by the same signer |
| `signoff-review` | error | XIV.14.7.11 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's review is not a closed review of the same revision by its hash |
| `signoff-required` | warning | XIV.14.7.12 | in 0.2, or in 0.1 with the proposal drawings: a sign-off the profile requires for the revision's status is not present, or withdrawn |
| `signoff-name` | warning | XIV.14.7.13 | in 0.2, or in 0.1 with the proposal drawings: a sign-off's file is not named s- and the first 12 hexadecimal digits of its envelope's hash |
| `rendition-unregistered` | error | XIV.15.2.1 | in 0.2, or in 0.1 with the proposal drawings: a PDF asset kept in the folder is named by no register |
| `rendition-unsigned` | warning | XIV.15.2.5 | in 0.2, or in 0.1 with the proposal drawings: a register keeps an unsigned rendition, which is derived |
| `model-review-schema` | error | XVIII.2.5.1 | in 0.2, or in 0.1 with the proposal review ([Proposals](#proposals)): a model review, `project/reviews/<review>.json`, does not read, names another format, does not validate against its schema, or its id is not its name read back |
| `model-review-code` | error | XVIII.2.5.2 | in 0.2, or in 0.1 with the proposal review: a model review's profile, an item's state or a decision is not the drawing profile's (where it lists no `reviewStates` or no `decisions`, none is), or the folder holds no drawing profile |
| `model-review-reviewer` | error | XVIII.2.5.3 | in 0.2, or in 0.1 with the proposal review: a model review's reviewer is not a person of the registry of the kind `person`: an agent gives no decision |
| `model-review-anchor` | error | XVIII.2.5.4 | in 0.2, or in 0.1 with the proposal review: an anchor names an element the reviewed version does not hold (a warning), or a hash that is not its record's there (by `checkReviewAnchors`, `src/review-anchors.js`, with that version, not `checkFolder`) |
| `model-review-after-close` | error | XVIII.2.5.5 | in 0.2, or in 0.1 with the proposal review: an item, reply, state or decision of a closed model review is later than its close |
| `model-review-carried-from` | warning | XVIII.2.5.6 | in 0.2, or in 0.1 with the proposal review: an item's `carriedFrom` names no item of another review, a model review or a drawing's, that the folder holds |
| `model-review-camera` | error | XVIII.2.5.7 | in 0.2, or in 0.1 with the proposal review: a viewpoint's camera has a direction or an up that is zero, or the two are parallel, or it gives the field of view or the height its projection does not take |
| `model-review-section` | error | XVIII.2.5.8 | in 0.2, or in 0.1 with the proposal review: a viewpoint's section has a plane whose normal is zero, or a box whose min is not below its max on each axis |
| `model-review-shape` | error | XVIII.2.5.9 | in 0.2, or in 0.1 with the proposal review: a model review item's shape breaks its form, as XIV.13.4, rule 9 says |
| `model-review-issue` | error | XVIII.2.5.10 | in 0.2, or in 0.1 with the proposal review: a model review's `issue` names no issue of the folder by its record's hash, or one that does not release the reviewed version |
| `issue-version-twice` | error | XVIII.3.2.1 | in 0.2, or in 0.1 with the proposal review: an issue lists a version twice, by its root |
| `issue-rendition-of` | error | XVIII.3.2.2 | in 0.2, or in 0.1 with the proposal review: an issue's rendition (a print) was printed from a version or a revision that the issue does not release |
| `issue-version-unknown` | error | XVIII.3.2.4 | in 0.2, or in 0.1 with the proposal review: a version an issue releases is not of the building's history. Needs the history: no static check reports it |
| `issue-signoff-shape` | error | XVIII.4.3.1 | in 0.2, or in 0.1 with the proposal review: a statement about an issue, `drawings/issues/<issue>/signoffs/<n>.json`, does not validate, its payload is no in-toto Statement v1, its predicateType is not `https://openeik.org/attestation/issue/v0.1`, or its predicate does not validate |
| `issue-signoff-subject` | error | XVIII.4.3.2 | in 0.2, or in 0.1 with the proposal review: a statement about an issue's subject is not exactly the issue record of its folder by its hash, or its predicate's issue is not that issue |
| `issue-signoff-signer` | error | XVIII.4.3.3 | in 0.2, or in 0.1 with the proposal review: a statement about an issue's signer is no person of the registry of the kind `person`, or a signature's keyid no key of that record |
| `issue-signoff-action` | error | XVIII.4.3.4 | in 0.2, or in 0.1 with the proposal review: an `issued` statement is not signed by the issue's `sent.by`, or a `received` one neither by a recipient the issue names by `person` nor by a person a receipt of the issue names as its recipient |
| `issue-signoff-timestamp` | error | XVIII.4.3.5 | in 0.2, or in 0.1 with the proposal review: a signature of a statement about an issue has no time-stamp |
| `issue-signoff-signature` | error | XVIII.4.3.6 | in 0.2, or in 0.1 with the proposal review: a statement about an issue's signature or time-stamp does not verify, or the key was not valid at the time-stamp's time (by `verifyReviewStatements`, `src/review-verify.js`, not `checkFolder`) |
| `issue-signoff-withdraws` | error | XVIII.4.3.7 | in 0.2, or in 0.1 with the proposal review: a withdrawal names no statement of the same issue by the same signer |
| `issue-signoff-name` | warning | XVIII.4.3.8 | in 0.2, or in 0.1 with the proposal review: a statement about an issue is not named `s-` and the first 12 hexadecimal digits of its envelope's hash |
| `receipt-shape` | error | XVIII.5.3.1 | in 0.2, or in 0.1 with the proposal review: a host's receipt, `drawings/issues/<issue>/receipts/<n>.json`, does not validate, its payload is no in-toto Statement v1, its predicateType is not `https://openeik.org/attestation/issue-receipt/v0.1`, or its predicate does not validate |
| `receipt-subject` | error | XVIII.5.3.2 | in 0.2, or in 0.1 with the proposal review: a receipt's subject is not exactly the issue record of its folder by its hash, or its predicate's issue is not that issue |
| `receipt-recipient` | error | XVIII.5.3.3 | in 0.2, or in 0.1 with the proposal review: a receipt's recipient is not one the issue names, as it names them, and the receipt gives no `sentBy` or no `transmittal`, which a recipient the host sent the issue to needs |
| `receipt-event-order` | error | XVIII.5.3.4 | in 0.2, or in 0.1 with the proposal review: a receipt's events are not in the order of their times, or one is later than the receipt |
| `receipt-event-unknown` | error | XVIII.5.3.5 | in 0.2, or in 0.1 with the proposal review: a receipt's event names a revision or a version the issue does not release, a print it does not name and gives no `of`, or, with `of`, a print of what it does not release |
| `receipt-timestamp` | error | XVIII.5.3.6 | in 0.2, or in 0.1 with the proposal review: a signature of a receipt has no time-stamp |
| `receipt-signature` | error | XVIII.5.3.7 | in 0.2, or in 0.1 with the proposal review: a receipt's signature or time-stamp does not verify with a key of a host's file of keys given whose `host.name` is the receipt's (XVIII.5.6), or the key was not valid at the time-stamp's time (by `verifyReviewStatements` and `verifyReceipts`, `src/review-verify.js`, not `checkFolder`) |
| `receipt-earlier` | warning | XVIII.5.3.8 | in 0.2, or in 0.1 with the proposal review: a host's later receipt for a recipient does not hold every event of that host's earlier one, the receipts of one host told by their `host.name`; two hosts' receipts are not compared |
| `receipt-name` | warning | XVIII.5.3.9 | in 0.2, or in 0.1 with the proposal review: a receipt is not named `s-` and the first 12 hexadecimal digits of its envelope's hash |
| `terms-shape` | error | XVIII.6.2.1 | in 0.2, or in 0.1 with the proposal review: a record of terms accepted, `project/terms-accepted/<n>.json`, does not validate, its payload is no in-toto Statement v1, its predicateType is not `https://openeik.org/attestation/terms-accepted/v0.1`, or its predicate does not validate |
| `terms-subject` | error | XVIII.6.2.2 | in 0.2, or in 0.1 with the proposal review: terms accepted name other subjects than the terms by their digest and, for an issue, the issue record by its hash |
| `terms-person` | error | XVIII.6.2.3 | in 0.2, or in 0.1 with the proposal review: the person who accepted terms is no person of the registry of the kind `person` |
| `terms-signer` | error | XVIII.6.2.4 | in 0.2, or in 0.1 with the proposal review: terms accepted by a person's own signature (`by` `person`) are signed with a key that is not that person's |
| `terms-for` | error | XVIII.6.2.5 | in 0.2, or in 0.1 with the proposal review: terms accepted for a project that is not the building's, or for an issue the folder does not hold by that hash |
| `terms-timestamp` | error | XVIII.6.2.6 | in 0.2, or in 0.1 with the proposal review: a signature of a record of terms accepted has no time-stamp |
| `terms-signature` | error | XVIII.6.2.7 | in 0.2, or in 0.1 with the proposal review: a record of terms accepted's signature or time-stamp does not verify, by the person's key or, attested `by` `host`, the host's (XVIII.5.6), or the key was not valid at the time-stamp's time (by `verifyReviewStatements`, `src/review-verify.js`, not `checkFolder`) |
| `terms-name` | warning | XVIII.6.2.8 | in 0.2, or in 0.1 with the proposal review: a record of terms accepted is not named `s-` and the first 12 hexadecimal digits of its envelope's hash |
| `owners-schema` | error | XVI.2.1 | in 0.2, or in 0.1 with the proposal ownership: `project/owners.json` does not read as I-JSON, names another format, does not validate against its schema, or is not in the stored form (then `stored-form`) |
| `owners-party-twice` | error | XVI.2.2 | in 0.2, or in 0.1 with the proposal ownership: a party's id is in the owners file twice |
| `owners-party-unknown` | error | XVI.2.2 | in 0.2, or in 0.1 with the proposal ownership: a rule's owners name a party the owners file does not have |
| `owners-glob` | error | XVI.3.2 | in 0.2, or in 0.1 with the proposal ownership: a rule's path is no glob (empty, a `/` at either end, an empty segment, `.` or `..`) |
| `layout-mixed` | warning | III.4.4.7 | in 0.2, or in 0.1 with the proposal ownership: a folder holds records in both layouts, some where only 0.1's puts them and others where only the owners' does |
| `asset-name` | error | XV.2.3 | in 0.2, or in 0.1 with the proposal assets: a file of `assets/` is not named by the SHA-256 of its bytes and an extension: no asset, left alone, never in a version |
| `asset-folder` | error | XV.2.1 | in 0.2, or in 0.1 with the proposal assets: `assets/` holds a folder |
| `asset-kind` | error | XV.4.1 | in 0.2, or in 0.1 with the proposal assets: an asset's extension is none of `pdf`, `ttf`, `otf`, `asice`, `png`, `jpg` |
| `asset-size` | error | XV.4.2 | in 0.2, or in 0.1 with the proposal assets: a file of `assets/` larger than 64 MiB |
| `asset-start` | warning | XV.4.1 | in 0.2, or in 0.1 with the proposal assets: an asset's bytes do not begin as its kind's do |
| `asset-pointer` | error | XV.3.2 | in 0.2, or in 0.1 with the proposal assets, reading a version: a leaf under `assets` is no pointer in the canonical form, names other bytes than its name's, or sits in a folder there; the version is refused |
| `asset-missing` | error | XV.6.3 | in 0.2, or in 0.1 with the proposal assets: a clone or a pull finds the store without the bytes of an asset the version names; nothing is written |
| `asset-mismatch` | error | XV.5.1 | in 0.2, or in 0.1 with the proposal assets: the store answered an asset with bytes that are not its pointer's; nothing is written |
| `asset-removed` | error | XV.3.7 | in 0.2, or in 0.1 with the proposal assets: a pull's merge would remove an asset that the merged model names; nothing is written |

The rules of heating, ventilation and plumbing (`src/mep.js`, Part II, 6.9 and 13) check a run's insulation, a component in its pipe or its duct (its host, its `along`, the place its run gives it, in a duct its size across, its type's ports), room tags and lines joined to symbols, across a view's detail items. The rules that need geometry or the types' sizes (`src/rules.js`) read the geometry Part IV states from the records alone: outlines (their area, the gaps between their points, edges that cross or touch), a component's two points, doors and windows in their walls (the wall's length, its base, its height from its top constraint or its attached top where that rises higher, and overlaps), tops above bases, the sizes types set, sections and layers, attached column bases and wall tops over their hosts (footprints, and the faces of flat hosts and of pitched roofs, Part IV, 4.4 and 5.3), outline edges trimmed to a wall's face (Part II, 6.7: the edge, the wall, parallel within 5°, reaching the face within 1 mm, 100 mm² left), sloped beams and member joins. Bodies, the joins found from geometry and quantities are a compiler's, and are not built here. A comparison the specification states exactly allows 10⁻⁶ mm for rounding, and a point within 0.001 mm of a boundary is on it.

### Command codes

In 0.2, and in 0.1 with the proposal envelope (Part XVII), each refusal of `applyCommands` carries a code: one of these, the processor's own, or the validator's code above for a rule the validator checks too. Of the envelope's codes (rules XVII), this package gives `envelope-schema`, `envelope-format`, `operation-reused`, `target-unknown` and `base-moved`; the others are a host's: who signed in, what it lets a writer do, what it holds in progress, and its limits. A refusal's pointer names the member that is wrong, a required one that is missing too (`/intent`, `/expectedRevision`).

| Code | Rule | What |
|---|---|---|
| `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, in kind |
| `kind-unknown` | V.1.2 | a command's kind is none of the vocabulary's |
| `kind-not-implemented` | V.2 | a command's kind is of a profile or of the reference vocabulary that the processor does not implement |
| `command-member-unknown` | V.1.4 | a command holds a member at its top level that its kind does not define, as a misspelt expectedRevison |
| `command-schema` | V.1.2 | a command does not match its kind's schema |
| `command-not-json` | V.8.2 | a command holds what JSON cannot: NaN, an infinity, a BigInt, or itself |
| `command-too-deep` | V.8.2 | a command nests deeper than any record may |
| `patch-undefined` | V.4.3 | a patch says undefined for a member its record has, which a logged command would not remove: send null |
| `patch-fixed` | V.4.2 | a patch writes an element's id, category or revision |
| `patch-legacy` | V.4.6 | a patch writes topRoofId, topCeilingId or roofCutId |
| `patch-removes` | V.4.2 | a patch takes away, with null, a member every element has |
| `target-missing` | V.6.2 | the record a command changes, deletes, copies or names as its target is not in the model |
| `element-of-copy` | II.10 | a command names an element of a placed copy, which is worked out from its original and never stored |
| `revision-conflict` | V.3.1 | a precondition names a revision, and the record's revision is another: it changed since it was read |
| `state-conflict` | V.3.3 | a precondition names a state (a level, the project's name, its site …), and the state is another: it changed since it was read |
| `precondition-missing` | V.3.2 | a command gives no precondition where one is required: a strict upsert of a record that exists, or any command in an envelope (Part XVII, 9) |
| `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, a level that is gone |
| `in-use` | V.5.4 | a delete is refused while records use what it would remove: a type, a level and its plans |
| `level-last` | II.3 | a command would remove a project's last level |
| `level-move-to` | V.5.4 | a level's delete names in moveTo no other level of the project |
| `group-member` | II.10 | a command changes an element of a group other than in the group's edit (original.edit) |
| `group-by-hand` | II.10 | a command writes original, copy or born itself: an element joins a group by original.make and original.add, and an instance is placed |
| `group-twice` | II.10 | an element, or one that stands in it, is of another group already: an element is in one group |
| `group-not-member` | II.10 | a command takes out of a group an element that is not of it |
| `group-hosted` | II.10 | a command takes out of a group an element that stands in another element of the group, without that element |
| `group-instances` | II.10 | a group is dissolved while it has other instances |
| `group-origin` | II.10 | a group is made of elements none of which has a place of its own, and no origin is given |
| `edit-nested` | II.10 | an original.edit holds another |
| `copies-not-derived` | II.10 | a command needs a copy's elements worked out, which the processor does not derive |
| `envelope-schema` | XVII.2 | an envelope does not match its schema |
| `envelope-format` | XVII.2 | an envelope names a format the processor does not implement |
| `envelope-required` | XVII.8 | a host takes this writer's commands only in an envelope |
| `operation-reused` | XVII.3 | an operation id the processor has recorded comes with another envelope |
| `operation-in-progress` | XVII.3 | an operation id comes again while the processor is still applying it |
| `person-mismatch` | XVII.4.2 | an envelope names another person than the one its writer signed in as, or one they may act for |
| `target-unknown` | XVII.5 | an envelope's target names a ref the processor does not have, or one it does not write |
| `base-moved` | XVII.5 | the target's ref has moved since the envelope's base, and the envelope asks for its base exactly |
| `mode-not-implemented` | XVII.6.4 | an envelope asks for a mode the processor does not implement |
| `mode-not-allowed` | XVII.6.4 | a host does not let this writer apply to this ref, and the writer did not ask for a proposal |
| `not-permitted` | XVII.12 | the writer may not change the target, or a record the commands change |
| `too-large` | XVII.12 | an envelope holds more commands or bytes than the processor takes in one |

## Model quality

`openeik check --quality` (or `checkFolder(…, {quality: true})`, `checkModel(…, {quality: true})`, `checkQuality(model)`) also warns about elements modelled under a category that does not fit them: a railing drawn as a generic component or as a thin wall, a window or a door as a box set in a wall, a stair as slabs stacked in steps, a beam as a component or a wall. A supplier invited to a project finds its parts by category (spec, Part II, 12), and so does every check that works by category; a part under the wrong category is missed by both. The warnings tell the model's author which category fits, and why.

**Off by default.** `check` is the validator of the specification: its errors and warnings name rules a model must or should meet, and "0 errors, 0 warnings" means that for writers and for CI. The quality warnings are heuristics about modelling practice, not rules, and a model can be valid with them. So they come only when asked for. They are warnings: they never change `ok` or the exit code.

**What counts as evidence.** Each rule of `QUALITY_RULES` names the category that fits:

| Code | Fits | A name is evidence for | A shape alone is evidence for | The shape |
|---|---|---|---|---|
| `quality-likely-railing` | `railing` | component, furniture, generic, wall, beam | a plain component, furniture, a generic element, a wall | long and thin (at most 120 mm; a wall at most 60 mm; at least 800 mm and 8 times its thickness long), its top 850 to 1300 mm above a slab's top, its bottom no more than 600 mm below that top or 300 mm above it, and along the slab's edge: within 10° and 300 mm, over at least half its length |
| `quality-likely-window` | `window` | component, furniture, generic, wall | a plain component, furniture, a generic element | 300 to 4000 mm wide and 300 to 2600 mm high, set in a wall (within 5° of it, its middle within the wall's thickness, its ends within the wall's length, no deeper than the wall plus 100 mm, within the wall's height), its bottom at least 100 mm above the wall's base |
| `quality-likely-door` | `door` | component, furniture, generic, wall | a plain component, furniture, a generic element | 600 to 2500 mm wide and 1900 to 2700 mm high, set in a wall as a window is, standing on the wall's base (within 50 mm) |
| `quality-likely-stair` | `stair` | component, furniture, generic, slab | a plain component, a generic element, a slab | one of three or more tread-like pieces (600 to 3000 mm by 180 to 450 mm, at most 300 mm thick; a slab's outline a rectangle) that rise by 120 to 220 mm each and step 180 to 450 mm along, parallel (within 3°) and in line (within 100 mm), with an even rise (within 10 mm) and going (within 25 mm) |
| `quality-likely-beam` | `beam` | component, furniture, generic, wall, slab | a plain component, a generic element | a horizontal member at least 1500 mm long, 90 mm wide and 140 mm deep, and at least 6 times as long as its larger side |

- **A name**: the element's name or its type's name names the category, in English and, for the common words, in Swedish, Norwegian, Danish, German, Dutch and French (`railing`, `balustrade`, `handrail`, `räcke`, `Geländer`, `leuning` …; `window`, `fönster`, `raam` …). The lists are in `src/quality.js`. Words match whole; a few heads also match at the end of a compound word (`balkongräcke`, `Wendeltreppe`, `stålbalk`), but never inside one (`outdoor` is no door, `Balkong` no beam). Camel case and underscores split words (`GlassBalustrade_1100`).
- **An IFC class**: the element was imported as `IfcRailing`, `IfcWindow`, `IfcDoor`, `IfcStair` or `IfcStairFlight`, or `IfcBeam` (`ifcSource.entity`).
- **A shape**: only on elements whose category says little about them: a component that is a plain box (the `timberSection` family, not a bolt, a plate, a hanger or a cast-in item), furniture, or an IFC proxy (`IfcBuildingElementProxy`); and, where the table says so, a wall or slabs. The shapes are read from the records alone, without a compiler: a wall's centreline, thickness (its type's layers) and height; a beam's axis and section; a component as a box `width` along its orientation, its type's `component.depth` across and `height` up, centred on its insertion point, as the reference implementation draws one; furniture placed along a line; a slab's outline and top.

**Keeping false positives low.**

- A name that names a part, a fitting or a place of a thing is no evidence: `door handle`, `window sill`, `joist hanger`, `railing post`, `stair landing`, `Fönsterbänk`, `Dörrhandtag`.
- A name that names the element's own category is no evidence: a wall named `Stair wall`, `Window wall` or `Balcony parapet` stays a wall.
- A name is read up to its first preposition, and its last category word decides: `Stair railing` and `Railing for stair` are railings, `Door lintel` and `Lintel over door` beams.
- A shape alone counts only on generic elements (and on walls for railings, at most 60 mm thick: glass or sheet, not a parapet; on slabs for stairs). A low wall, a batten, a bookshelf along a balcony and two steps are not flagged.
- Each element gets at most one warning. When rules disagree, the heaviest evidence decides: the element's own name, then its type's name, then its IFC class, then its shape.
- On the two examples here and on the nine synthetic example projects of the reference implementation (about 830 elements, with components, slab edges, beams, walls and openings), `--quality` finds nothing.

**What it does not do.** It reads no derived geometry, so it does not see curved or sloped stand-ins, stairs that turn, railings on a stair, on a shaft's edge or on a wall's top, or a window drawn as a void. It does not move anything: the fix is the author's (a new element of the category that fits, with its type), or a supplier's proposal.

Each warning carries `data`: `{elementId, category, fits, evidence}`, `evidence` being the kinds that counted (`name`, `type`, `ifc`, `shape`). The message says which category fits and why, for example:

```text
architecture/level-1/component/item-17.json: likely a railing, modelled as a component: 3000 mm long and 40 mm thick,
its top 1100 mm above slab "slab-balcony", along its edge "slab-balcony:edge:0". Category railing fits, and a search
for railings by category misses it [quality-likely-railing, II.6.2]
```

Their rule is Part II, 6.2, the categories; they are not requirements of it.

## Merging

A three-way merge joins two lines of work, ours and theirs, against the version both started from (spec, Part VI, 5). Records are matched by id, never by path or text, so a wall moved to another level on one side and renamed on the other is one record.

```js
import { mergeFolders, mergeVersions, resolveMerge } from "@openeik/tools";

const result = mergeFolders("base", "ours", "theirs");  // or mergeModels(base, ours, theirs) on single-file models
result.ok;           // true: no conflict is left, and the merged model checks whole
result.model;        // the merged model, in the single-file form
result.merged;       // what it took: [{list: "elements", id: "wall-east", from: "theirs", kind: "changed"}, …]
result.conflicts;    // [{key: "elements/wall-north", list, id, kind: "both changed", path: "/height", base: 2700, ours: 2800, theirs: 2900}]
result.diagnostics;  // the check, named by record: {code: "ref-missing", path: "elements/window-3", …}

// A person chooses ours or theirs for a conflicted record; the merged model is checked again.
const settled = resolveMerge(result, { "elements/wall-north": "theirs" });

// Two versions of a store: the base is found from the commits' parents.
const merge = await mergeVersions(store, { ours: "main", theirs: "alternatives/frame-a" });
```

- A record changed on one side only is taken from that side. Changed on both, it merges member by member: objects recursively, a list whose items all have an `id` (a type's layers, a view's detail items) item by item, in our order with their new items after the item before them on their side, and anything else (a number, a point list) as a whole. A record merged from both sides gets the revision max(ours, theirs) + 1.
- The conflicts are `both changed` (with the member's path), `changed and deleted`, `added twice`, and `in the way`: a kept file of one side where the other side's records or kept files need its path (Part III, 7.4). A conflicted record keeps our side until a choice settles it. A record settled by a choice, which both sides hold, keeps the chosen side's revision when that is the higher; otherwise it gets the revision past both (5.4), so that no earlier read of either side's record can name it. The project record and the project information merge as one record each; the levels as a list by id, with one conflict for the whole list; kept files file by file.
- The merged model must pass the whole-model check: a merge that leaves a host missing, or a type deleted under its elements, is not `ok`, and its diagnostics name the records.
- With `commit: {author, write}`, `mergeVersions` writes a merge commit (`kind: "merge"`, two parents) when the merge is clean and checks, and moves the ref by compare-and-swap from ours' version (`pushVersion` takes `parents` and `expected` for it). `write` turns the merged model into a folder's files; by default this package's writer (`modelFiles`, with ours' `eik.json`).
- When two lines have merged each other, `mergeBase` takes the common ancestor with the fewest parent steps from both, then the newest, then the smallest hash.

## Remotes: clone, push, pull

A folder works with a store (API v0) as a git working copy works with a remote (spec, Part VI, 6): cloned from a ref once, edited locally with any tool, pushed deliberately with a message, and pulled to bring in what others pushed.

```js
import { connectStore, clone, push, pull, status } from "@openeik/tools";

const store = connectStore({ url: "https://api.eikhub.com", repo: "acme/house", token: process.env.OPENEIK_TOKEN });
await clone(store, "./house");             // main's head into a new folder, tracked in ./house/.eik/remotes.json
// … edit records in ./house, and check them …
status("./house").changed;                 // true: something to push (local: the store is not asked)
await push(store, "./house", { message: "Taller south wall", author: "Jane Doe" });
// {pushed: true, version, parent: the version cloned, …}; or code "ref-moved": someone pushed meanwhile, pull first
const r = await pull(store, "./house");    // r.status: "up-to-date", "fast-forward", "merged", "conflicts" or "refused"
if (r.status === "conflicts") await pull(store, "./house", { choices: { "elements/wall-north": "theirs" } });
```

- **The tracking ref.** `.eik/remotes.json` records, per remote and ref, the version last fetched from or pushed to it, and the folder's tree when it held that version: `{"origin": {"url": "…", "repo": "acme/house", "refs": {"main": {"version": "<hash>", "tree": "<hash>"}}}}`, in the stored form. It is hidden, so no part of the model or of any version's tree, and never sent to the store. The token is never in it: it comes from `OPENEIK_TOKEN`, or the variable `--token-env` names, and goes to the remote's url only at the origin given beside it ([Tokens](#tokens)). `origin` is the default remote; the default ref is the one ref the remote tracks, else `main`.
- **clone** reads the ref's head, writes it into an empty or new folder (hidden files aside) through the writer, checked whole, `AGENTS.md` written, and records the tracking ref. Its tree is the folder's, as `versionTree` computes it: the version's own, unless the version leaves `eik.json` out of its tree, as versions saved before 11 October 2026 do. A ref without a version is not cloned: push a first version with `pushVersion` (`openeik push --url --repo`) and clone that.
- **push** compares the folder's tree with the tracked tree. Equal: nothing to push, and the store is not asked. Otherwise one version: `kind: "named"`, named by the message, its tree the folder's, its only parent the tracked version, its summary every change since that version, whatever states the folder passed through; only the objects the store lacks are sent. The ref moves by compare-and-swap from the tracked version. When it moved meanwhile, nothing is written, and the error (`ref-moved`, with `current`) says to pull first. Then the tracking ref is the new version. A push never makes an automatic version, and never rewrites one.
- **pull**: the base is the tracked version, theirs the ref's head. Theirs the base: nothing to do. Theirs must descend from the base: a ref moved back, or onto a line that never held the tracked version, is refused (`not-descendant`, with `current` and `tracked`), nothing written and the tracking ref left as it was, since a remote ref only moves forward (Part VI, 6.3, rule 2) and a fast-forward or a merge against the base would undo, in silence, what theirs no longer holds; clone the ref into a new folder to take it as it is. The folder unchanged since the base: a fast-forward, theirs written through the writer. Otherwise the folder (ours) and theirs merge against the base by record and member ([Merging](#merging)); merged cleanly and checked whole, the merge is written, and the tracking ref becomes theirs, so that the next push's parent is theirs and its tree the merged one. While a conflict is left, or the merged model does not check, nothing is written: the result lists the conflicts and holds the merge, and a pull with `choices` (as `resolveMerge` takes them) settles them.
- **What a pull writes**: only the files that change (Part III, 13). The files of records that went are removed, and so is a kept file theirs removed that the folder had not changed (the history removed it; the next push would otherwise bring it back). Kept files theirs did not change, files the reader leaves alone (binary, over 1 MiB), `AGENTS.md` and hidden files are not touched. Kept files and members this package does not know come in and go back unchanged.
- **status** is local and cheap: per remote and ref, the tracked version, and whether the folder changed since. `fetchHead(store, ref)` asks the store for the ref's head in one call, to say whether the remote moved; `openeik status --fetch` does.

From the command line:

```sh
export OPENEIK_TOKEN=…  OPENEIK_TOKEN_ORIGIN=https://api.eikhub.com  OPENEIK_AUTHOR="Jane Doe"
openeik clone ./house --url https://api.eikhub.com --repo acme/house
openeik status ./house                       # the version it tracks, and whether it changed since
openeik push ./house -m "Taller south wall"  # exit 1, nothing written, when the ref moved: pull first
openeik pull ./house                         # a fast-forward or a merge; exit 1, nothing written, on a conflict:
openeik pull ./house --choose elements/wall-north=theirs
```

A folder with `.eik/remotes.json` pushes and pulls with its remote, and refuses the flags of the low-level commands (`--url`, `--repo`, `--parent`, `--name`, `--version`, `--force` …). A folder without one pushes and pulls as before.

### Tokens

A token is a credential: it goes only where the person who holds it sends it.

- **Over https.** `connectStore` refuses a token for a url that is not https (`insecure-url`), but for plain http to this computer (`localhost`, 127.0.0.0/8, `[::1]`), as a local store or a test server uses; an `Authorization` header in `headers` counts as a token. Nothing is sent first.
- **To its own origin.** With `tokenOrigin` (scheme, host and port: `https://api.eikhub.com`), a url of another origin is refused (`token-origin`), before anything is sent. A url read from a folder (`.eik/remotes.json`, which travels with the folder in an archive or a git clone, and may come from anyone) is connected so.
- **The command.** `openeik` reads the token from `OPENEIK_TOKEN` (or the variable `--token-env <NAME>` names) and the origin it is for from `OPENEIK_TOKEN_ORIGIN` (`<NAME>_ORIGIN`). A store named on the command line (`--url`: `clone`, and the low-level `push`, `pull` and `log`) gets the token, unless `OPENEIK_TOKEN_ORIGIN` names another origin. A folder's remote (`push`, `pull` and `status --fetch` of a folder with `.eik/remotes.json`) gets it only at the origin `OPENEIK_TOKEN_ORIGIN` names: without one, or at another origin, the command stops and says why (exit 2), and nothing is sent.
- **Anonymous reads stay open.** Without a token, any url is read as anyone may read it: a public repository clones, pulls and fetches with no token at all.
- **Names in a store's URL.** A repository (`owner/name`), a ref (`main`, `alternatives/<slug>`) and a meta key are made of plain segments: none empty, `.` or `..`, none holding `\` or NUL, and `/` only between a ref's segments. `connectStore` refuses another repository name (`bad-repo`), each call another ref (`bad-ref`) or key (`bad-key`), before anything is sent, and `readRemotes` refuses a folder's remotes that name one (`remotes-invalid`): `refs/../../../admin/users` would ask another part of the store's API, with the token.

### Reading a version that no folder gives

A reader of versions refuses a version whose tree no folder gives (Security and privacy, 5.3, rule 2): a reader of a folder would leave such an entry out, or read it as another file, and the next version stored would drop it. Every such refusal, by code, with what triggers it:

| Code | What triggers it | Where a read that repairs puts it |
|---|---|---|
| `tree-name` | an entry's name that is no path segment: empty, `.` or `..`, or holding `/`, `\` or NUL | left out, given back |
| `tree-name` | a hidden name, one that starts with `.` (`.git`, `.draft.md`), which a reader of a folder skips (Part III, 1) | left out, given back |
| `tree-name` | a name that starts with `~` and is no shard: a leaf (`~x`), a sub-tree of a folder whose entries are not all shards, one not named `~` and two lowercase hexadecimal digits (`~zz`, `~CA`), or a shard in a shard (Part VI, 2, rule 3) | left out, given back (a sub-tree whole) |
| `tree-name` | a name in a shard other than the one its name's hash gives (rule 3) | read where it is, as the folder's entry, and reported |
| `tree-duplicate` | a name twice in one folder: in one tree, or in two of its shards (Part VI, 2) | the first read, each later one left out and given back |
| `tree-kept` | a kept file at a path that Part III, 7, rule 8 keeps from kept files: a name a file system refuses or reads otherwise (`notes/con.txt`, `notes/c:draft.md`, `C:/notes.md`), a path that folds to one with a record's, `eik.json`'s, `AGENTS.md`'s or an earlier kept file's (`Types/notes.md`, `agents.md`, `notes/Readme.md` beside `notes/README.md`), or one through `eik.json` or `AGENTS.md` (`AGENTS.md/notes.md`) | left out, given back |
| `tree-kept` | a kept file over 1 MiB, or holding NUL (rule 2); with the proposal drawings, an issued drawing over 64 MiB (XIV.7.3) | left out, given back |
| `tree-kept` | a leaf read as a kept file (a JSON string, or an object at no record's place, kept as its text) at a path no kept file has (rules 1 and 4): `AGENTS.md`, or a record's file, `<leaf>.json` where `<leaf>` is a record's place (`project/materials/notes.json`, `drawings/views/plan/v1/view.json`; with the proposal electrical, `project/circuits/<id>.json`) | left out, given back |

A tree or an entry of another shape is no tree (`not-a-tree`), and an object that does not hash to its name is no object (`hash-mismatch`): a read that repairs refuses both, as every read does.

`readVersion(source, version, {repair: true})` reads such a version all the same, and loses nothing of it (rule 4). Each entry that no folder gives is left out of the model, of `files` and of `leaves`, reported in `diagnostics` with its code (an error, the entry in its `data`: `{entry, kind, hash, placed}`), and given back in `unplaced`:

```js
const read = await readVersion(source, root, { repair: true, check: false, lenient: true });
read.unplaced;   // [{path: "agents.md", folder: "", name: "agents.md", kind: "b", hash: "…", code: "tree-kept", why: "a kept file whose path folds to one with AGENTS.md …"}]
read.leaves;     // Map: every other leaf, path to hash, as versionTree gives them
```

Whoever stores the next version from such a read holds each entry of `unplaced` in it, unchanged at its path (its object is in the store, by `hash`), unless the application that owns the file, or a person, renamed or removed it. `repair` is not `lenient`: a lenient read is never written back, while a read that repairs is made so that the next version can be stored from it, with what it gave back. A clone and a pull never repair: the folder they write could not hold what is left out. To find such versions without opening them, read each with `repair` and look at `unplaced`, or read it without, and look at the refusal's `code` and `diagnostics`.

Two more options of `readVersion` are for an application that keeps its own store:

- `verify: false` takes the objects as the source answers them, without hashing each again (rule 5): only for a store of the application's own that checked each object against its hash as it took it in, and refused what failed, or for objects it made itself from their text (`versionTree`, its own writer). Never for another store's: a store `connectStore` connects is refused (`verify-remote`). A cache filled so holds what the source gave, and is shared only with reads of that source.
- `leaves` (always given) is each leaf read, path to its object's hash, as `versionTree` gives them, and `hashOf(list, id)` the hash of the leaf a record is in: a writer that keeps them finds the records that did not change without hashing them again. The model stays the single-file form.

Each record is parsed once, from its object, and the files' texts are its stored form. On the reference implementation's examples, read from memory as it reads them (`check: false`, `lenient`, every proposal), this reads a version in 0.53 to 0.66 of the time it took before (the guesthouse villa, 1181 objects, 25 ms where it took 38; the villa, 910 objects and 5.5 MB, 87 ms where it took 148; a version of 16 597 objects, 171 ms where it took 259); `verify: false` saves another 1 to 6 %, since hashing every object was never the main cost. Most of what is left is the I-JSON check of every record (Security and privacy, 7.3) and the stored form of each record's file.

## Compiling

`compileModel(model)` (and `openeik compile`) derives from a model's records what the class-A rules of Part IV give: the bodies of walls, slabs, foundations, ceilings, roofs, beams, columns, slab edge elements, gutters, pipes and ducts, with the openings shafts cut in their hosts, their quantities, and members' reference lines and axes; the line of a railing on a slab's or a shaft's outline, on a wall's level top or along a slab edge element, and its given posts; and a supplier's declared geometry, placed in its frame, on any element, a generic element's only shape. These are the requirements of the compiler (core) class, which this package claims of OpenEik 0.1 and of 0.2 ([Conformance](#conformance)): Part IV, 1 (nothing derived is stored; unknown categories), 4, 5, 6, 11 and 12; and, of 0.2, the rules 0.2 adds to those sections (a slab on a sloped plane, a door's and a window's own body, a slab edge element's notches, a framing family's parts and its beam's lines, a member between two points), the class-A rules of Part IV, 7.3 to 7.6 (straight, L and U stairs, a framed wall layer's members, a roof's truss layout and its hip ends, a floor's joists, a room's boundary, ceiling and quantities, a component's frame and its bodies), which the class asks of 0.2 by erratum 0.2-E1 ([`errata/0.2.md`](../../errata/0.2.md)), and Part XIII's bodies (Conformance, 1.1), which it derives for every model of 0.2 ([Proposals](#proposals)). What it does not derive (below) is reference behaviour, which the class does not require. It is a pure function of the records: it changes nothing in the model, and the same records give the same result in any element order (1.2, 1.3). For `examples/one-storey-house` it gives the quantities worked in Part IV, 9.

```js
import { readFolder, compileModel } from "@openeik/tools";

const { bodies, quantities, lines, railings, declared, diagnostics, notes } = compileModel(readFolder("examples/one-storey-house").model);
quantities["wall-south"].grossArea;    // {value: 27.0358…, unit: "m2", basis: "one side, on the centreline: …", rule: "IV.4.1"}
quantities["roof-main"].ridge.value;   // 10.6 (m)
```

**What is derived, rule by rule.**

| Rule | Derived |
|---|---|
| 4.1 Walls | One body per layer of non-zero thickness, between d_1 = T/2 and d_(i+1) = d_i − t_i along the left normal; without layers, one body of the wall's `width`. Limited by its `wallOutline` and its top |
| 4.3 Openings | A door or window cuts, through every layer, the box from `along` to `along + width` and from `offset` to `offset + height` above the wall's base, or its type's `outline` (x from the opening's near edge, y up from its bottom) |
| 4.2 Slabs, foundations, ceilings, flat roofs | One body per layer, top first, down from the top face; a layer's `inset` offsets the outline inwards; `voids` cut through every layer; a round footing is a cylinder. A slab or a foundation is thickened under a slab edge element whose type has a `beam`, and fills a chamfer's wedge (4.6) |
| 4.2, rule 8 Shafts | Each shaft takes the prism of its outline, from its base to its top, out of every body of the slabs, ceilings and roofs it overlaps in height by more than 0.5 mm, and a stair's shaft out of a foundation's too; through a body where it reaches within 0.5 mm of its underside or top. `shaftOpenings` gives each opening, `through` where it takes all of its host within its outline, else a recess. A slab's, ceiling's or flat roof's `area` loses the shafts through its whole thickness |
| 4.4 Pitched roofs | One plane per slope-defining edge, the top surface the least of them, the outline pushed out by each edge's overhang; each layer between the planes lowered by its depths times √(1 + gx² + gy²) |
| 4.5 Edges trimmed to a wall's face | The outline, or a pitched roof's extent, cut along the line of each face an edge is trimmed to (`edgeAttachments`): the wall's baseline moved by half its thickness, to its left for the exterior face, to its right for the interior one. The side away from the wall is kept, before the layers, their insets and the voids, but for a pitched roof's insets, which are measured from its eaves and then trimmed at the face; a pitched roof's planes stay its recorded edges'. The record keeps its outline, and the walls and their types are sources of the result. `trimmedOutlineOf` gives the trimmed outline without compiling |
| 4.6 Slab edge elements | The section (a rectangle centred on the edge's line; an L's upstand outside it and its bottom leg inside; a U's second upstand), hung from the host's top or the underside of its structure, raised by `verticalOffset`, moved in by `horizontalOffset`, mirrored by `flipped`; its facing outside it, its upstand's chamfer cut; each rectangle a prism along the edge, its ends square or mitred on the bisector of the host's corner (`miterEnds`, else where the edge round the corner carries an element that mitres by itself; square where the edges run straight on, the corner is sharper than 2·arctan(1/4), or the element is too short for its mitres). The host's thickened edges in bands under its structure, its lower layers giving way, and the wedge of a chamfer filled by its structure |
| 4.7 Gutters | The section (a half-round about its back lip on the eave line, its arc in 16 straight pieces; or a box) along its roof's edge moved out by its overhang, from where the moved lines of the edges beside it meet it, its top at the roof's top surface at each end, moved out by its horizontal offsets (the other way where `flipped`) and up by its vertical ones; left out where another roof of its roof's `joinGroup` lies more than 20 mm over its roof's top, found exactly, and where a piece of 10 mm or less is left. Its `length` in plan and its `volume`, the section's exact area by that length |
| 5.1 Top constraints | The height from the constraint's level and offset |
| 5.2 Column bases | The host's top or underside at the column's centre, plus the offset; the top stays where the record puts it |
| 5.3 Attached tops | The host's face (its top, its underside or a layer's underside) lowered square to a pitched roof's planes and raised by the offset; raking (the body cut by the face's planes) or level (flat at the face's lowest point over the core band) |
| 6.1 to 6.3 Members | The section, or the member's own `width` and `height`, swept along the reference line, square to the set-out line; a column turned by `turn`, a beam rolled by `roll` and tapered by `taper` |
| 6.4 Member joins | A `mitre` cuts both members on the plane bisecting their reference lines at their corner; a `butt` stops the end at the face of `to` its reference line enters first, held back by `gap`. A joined end runs on to its cut, or is cut back to it; `extend: false` only cuts |
| 6.5 Lines | Each member's reference line, through the middle of its section, and its axis, through the centroid: the reference line moved by the section's centroid offset, turned by the roll or the turn |
| 11.2 Railing lines | On a slab's or a shaft's outline: the edges `railing.edges` names, in their order round the outline as one run (closed when it names them all), each moved towards the walking side (inside a slab, outside a shaft) by its inset (`insets[<edge id>]`, else `inset`, else 50 mm). With one inset for the whole run, every corner moves along its bisector by the inset over the cosine of half the turn, at most 4 times the inset; otherwise every corner is where the moved lines cross, or halfway between their ends where they are parallel or cross farther than 4 times the larger inset. The validator finds the same line, point for point. The line stands on the host's top (a shaft's: 5.1). A railing with no host runs along its profile, on its level plus offset |
| 11.2 Railing lines on a wall's top and along a slab edge element | On a wall's level top: along its baseline, `T/2 − inset` to its left (the middle of its top without an inset), on its top. Along a slab edge element: its outer face moved in by the inset (the middle of its top without one), between its own ends, square or mitred, on its top and never under its slab's |
| 11.2 Given posts | A prism at each distance in `railing.posts.at` along the line in plan, and nowhere else: `postWidth` along the line by `postDepth` across it (`post` by `post` without them), centred on the line, square to its segment, or within 0.5 mm of a corner to the corner's bisector; from the base (`belowBase` below it when `mount` is `side`) to the underside of the top rail, `height` less the rail's depth |
| 12 Pipes and ducts | The run's points at their heights above the level (`elev(levelId) + mep.z[i]`, the offset not added); its outer width `W = mep.size + 2·insulation` (a rectangular duct also `H = (mep.height ?? mep.size) + 2·insulation`); each corner an arc of radius `W`, its ends `W·tan(θ/2)` from the corner but at most half the shorter piece (the radius shrinking to fit), sampled every π/12 or less; a circle (20 points for a duct, 14 for a pipe) carried without twisting, or an upright rectangle whose width stays level, along the path; one closed mesh. Faceted as the reference implementation facets it, so that its numbers are the reference's |
| 12.6 Terminals and components in a pipe or a duct | A terminal's declared geometry, else a box of its type's `terminal.width` across the way it faces (`profile[0]` toward `profile[1]`, else plan +y), `terminal.depth` along it and `terminal.height` high, centred on its point, from `elev(levelId) + offset` up; a component in a pipe or a duct the same box on its run's axis at `along`, facing along the run, placed from the run rather than its stored profile and offset; in a duct `max(terminal.width, W + 20)` across and `max(terminal.height, H + 20)` high, `W` and `H` the duct's outer size, rather than its stored `width` and `height`. A duct part that names no duct is a terminal as any other |
| 11.1 Declared geometry | Each aspect (`envelope`, `fixings`, `clearances`, `positions`, `parts`) from the element's declaration, else its type's, placed in that declaration's frame: an element's `world` frame (plan coordinates, heights over its base) or `line` frame (`[s, w, z]` along its line); a type's frame at the element's insertion point (component, furniture, terminal, column, generic), in its opening (door, window: x along the wall from the near edge, y towards the exterior, z up from the opening's bottom) or as a `swept` section along its line (railing, beam). A placed envelope stands in place of the bodies its category's rules derive; `loads` and `fabrication` are passed on as declared |
| 11.3 Generic elements | Their declared bodies only, at the insertion point, turned to the orientation, on the base; a volume from the closed ones with the basis `declared` |
| 1, rule 4 Unknown categories | Kept, with no body but their own declared geometry in the `world` frame; a type's declaration is not placed, since its frame depends on its category |
| XIII.7 and 6.3 | A device's box, a carrier's or a cable's run, and each modelled leg's length ([Proposals](#proposals)) |

**Bodies** are JSON, in millimetres, in project coordinates with z up. Each names its `element`, and its `layer` where the type has layers. A railing's post names its `part` (`"post"`) and where it stands along the line (`at`), and a slab edge element's facing its `part` (`"facing"`); a chamfer's fill and a band of a thickened edge are bodies of their host's structure layer; a body of declared geometry names whose declaration it comes from (`declared`: `"element"` or `"type"`) and its `aspect` (`"envelope"`; in `declared`, `"clearances"` or `"parts"`).

- `prism` (N1, and N3 with planes): `{profile, holes?, bottom?, top?, bottomPlanes?, topPlanes?}`: the plan polygon `profile` (counter-clockwise) less its `holes` (clockwise), from its underside up to its top. The top is the least of `top` and the planes of `topPlanes` at each point; the underside is `bottom`, or the least of `bottomPlanes`. A plane is `{gx, gy, z0}`: z = z0 + gx·x + gy·y. Where the underside is above the top, the body is empty. A wall's layer is one prism per span between, under and over its openings; a pitched roof's layer is one prism between the planes of its two surfaces.
- `sweep` (N4): `{line, across, up, section, extent?, cuts?}`: the section swept along `line`, the member's reference line from its start to its end. A point (x, v) of the section, x across and v up from the middle of its bounding box, lies at line(λ) + across·x + up·v. A tapered section's depth runs from `section.depth` at λ = 0 to `section.endDepth` at λ = 1, its underside on the set-out line. `extent` (default [0, 1]) is the range of λ swept, longer where a joined end runs on to its cut; a cut `{point, normal}` keeps the points p with (p − point)·normal ≤ 0.
- `cylinder` (N5): `{centre, radius, bottom, top}`, a round footing or a declared cylinder; `rod` (N5): `{from, to, diameter}`, a declared round bar.
- `polyhedron` (N2, convex) and `mesh` (N6): `{vertices, faces}`, each face a list of vertex indices, counter-clockwise seen from outside where the body is closed. A declared `swept` body is placed as one closed `mesh` per segment of its line, `at` the distance where the segment starts. A pipe or a duct is one closed `mesh`: a ring of points at each point of its rounded path, joined by four-sided faces, its first ring (reversed) and its last closing its ends.

**Quantities** are `{value, unit, basis, rule}`, in m³, m² or m, or a `count`, per element: its `volume` (openings and voids deducted, wall ends square, overlaps with other elements not deducted) and the volume of each of its `layers`; a wall's `length`, and its `grossArea`, `openingArea` and `netArea` on one side, on its centreline; a slab's, foundation's or ceiling's `area`; a pitched roof's `slopedArea` (its top surface over the outline extended by the overhangs, measured on the slope), `planArea`, `ridge` (the level lines where two of its planes meet on its top) and `hips` (the sloping ones); a member's `length`; a railing's `length` (its line in plan) and, with `posts.at`, its `posts`; a pipe's or a duct's `volume` (its outer section, insulation included, along its rounded path as sampled: not what the faceted mesh holds) and `length` (along its points in 3D, each bend as its corner). Where a declared envelope is placed, the `volume` is its closed bodies' (summed, overlaps not deducted), with the basis `declared`, and the volumes of the layers it replaces are not given; the quantities measured on the record (lengths, areas) stay. `lines` gives each member's `setOut` line (beams), `reference` line and `axis`, its section's `centroidOffset` in the section's own coordinates before roll or turn, and its `length`. Analysis tools use the axis (Part IV, 6.5). `railings` gives each railing's `line` (its points on its base, closing on its first where it is `closed`), its `length`, its walking side (`walk`: `left` or `right` of the line) and its `posts` (`at`, `centre`, the unit vector `along` its width, `width`, `depth`, `bottom`, `top`). `declared` gives, per element with declared geometry, each aspect's `from` (`element` or `type`), `level`, `tolerance` and `status` (`placed`, with its `frame`; `passed on`, for loads and fabrication; `kept`, with `why`), the placed `clearances`, `parts`, `fixings` and `positions`, the `loads` and `fabrication` as declared, and its `basis` records, each `confirmed` while its hash is unchanged. `sources` names, per element, the records its results come from.

**Where Part IV leaves a choice**, the compiler takes this one, and says so in its code:

- A raking attached top cuts the wall's bodies with the face's planes, so that across the wall the top follows the face; the areas are taken on the centreline. An attached wall's `wallOutline` is its unconnected outline (5.3.7) and is not applied. A level top's core band runs from the first core layer to the last, or across the whole wall without one.
- Each layer of a pitched roof lies between the least of its planes lowered by the layer's upper depth and the least lowered by its lower depth, as an attached face is found (5.3.3), so layers under faces of different slopes meet without a step. A pitched roof's layer `inset` is measured from the eaves (the extended outline), and a face its edges are trimmed to then trims the layer as it trims the extent (4.5, rule 4: the layers fill what is left), so that the layer reaches the face unless its inset lies beyond it; and `voids` cut pitched roofs too.
- An inset is mitred: each edge parallel to its own at the inset distance, corners where the moved edges meet. Where edges collapse, what turns inside out drops out, and a region that pinches off splits in two. There is no mitre limit.
- A roll turns right-handed about the set-out line's direction (counter-clockwise seen with the member coming towards you). A tapered beam turns about the line through the middle of its section at its start.
- An `L` section's legs meet at its `angleCorner`, bottom left (−x, the underside) by default; a `U`'s web is on its −x side; `radius` is a root fillet of an `L`, `U` or `I` (a `T`'s and a `2L`'s are where Part II, 5.2, rule 5 puts them), and an `RHS`'s outer corner radius (inside, the radius less the wall); a `CHS`'s `width` is its outer diameter and its `web` its wall, and a `round` section is the disc of its `width`. Areas and centroids are exact, arcs included.
- A mitre's corner is the middle of the closest points of the two reference lines. A butt meets the other member's body as built before its own joins; a round tube's or bar's face is the plane touching it where the reference line meets it. A joined end grows by at most the distance to the joint plus both sections' larger dimensions, as Part IV, 6.4 describes the reference behaviour. `gap` is a butt's (6.4.2).
- A host covers a wall's centreline, or a column's centre, within 0.001 mm: the boundary counts. A pitched roof's footprint is its outline extended by its overhangs.
- Over an outline with a re-entrant corner whose edges define slopes (an L), the least of the planes runs below the eaves inside the outline. The compiler builds what 4.4.2 says and warns (`roof-below-eaves`); such a roof is drawn as two roofs.
- A railing's corners take the bisector rule only where the whole run has one inset, as the reference implementation and the validator do; with two insets or more, a corner whose two edges share one takes the crossing rule too (the two differ at turns sharper than about 151°, where the bisector stops at 4 times the inset and the crossing gives way to the halfway point). A half-round top rail is half its `railWidth` deep, a flat one `railDepth`. A railing with no host walks on its line's left. A post stands at a corner when it is within 0.5 mm of it; given posts are placed on a railing with no host too, and a position off the line is reported (`railing-post-layout`), not placed.
- A railing on an outline measures its `line` frame's heights, and its type's section, from its base (the host's top); a beam's line is its plan axis, level at its base, and its `w` runs to its left. A line-frame body is mapped by the one segment that holds all its points within 0.5 mm, else point by point; a swept section is swept square to each segment, the pieces not mitred.
- A declared body's volume: a prism's area by its height, a cylinder's and a rod's exact, a closed mesh's by the divergence theorem (turned outwards first), a convex polyhedron's with its faces turned away from its centroid. A mesh whose faces do not pair up the other way round, or a polyhedron whose edges are not each met by two faces, is placed without a volume (`declared-open`).
- The opening frame does not turn with `flipped` or `handFlipped`; a column's point frame turns with its `turn` and stands on its attached base where it has one; a component placed by `insertAt`, a slab edge's or a gutter's line, and a type of any other category (a stair kit) have no frame here: their declarations are kept, not placed (`declared-frame`). A declaration's `basis` is looked for as the validator looks for it, among the elements, the types, the views and then the other lists; an id that names no record (an edge's) leaves it unconfirmed.

- A pipe or a duct whose points come within 1 mm of each other, or that turns straight back, is built by the rule and reported (`run-shape`): the reference implementation's validator refuses it. A run without `mep`, or without a height for each point, is not built (`run-heights`); the reference implementation reads it with fallbacks of its own (Part IV, 12, note).
- A rectangular duct's section keeps its side vector where the run is vertical and never reverses it (Part IV, 12.3, rule 3). The reference implementation does the same since `d0774a48`; all eighteen fixture runs (`test/fixtures/run-bodies.json`) are its own numbers.

**Not derived** (reference behaviour, classes B and C; Part IV, 2, 7.1 and 7.2): wall joins (the ends are square, and the result's `notes` say so), framing families (a beam built from one has no body or lines), a `sheet` section's profile (Part IV does not fix one: no area, volume or axis), trusses and timber framing, roof windows, join groups, vaults, square edge cuts and layer extents, slab joints, panels and edge attachments, recesses and edge details, a wall's cut roof and extended exterior layers, column connections, a slab edge element's notches and a framing family's section, and the bodies of doors and windows, components, stairs, rebar, mesh and site elements, but for their declared geometry; a gutter on a barrel vault; a shaft's cut in a round footing or in a chamfer's fill (`shaft-uncut`); the shapes the reference implementation draws for some terminals (a terminal is a box of its type's size, 12.6); the MEP fittings between runs (branches, reductions, joints) and a downpipe's path from its gutter; a railing's line on a stair, its post layout without `posts.at` (`railing-posts-reference`), its rails, infill and handrails; room boundaries; placed copies. An element that uses one of them gets an info diagnostic (`not-derived`), and the `notes` list what the model leaves out. Of these, it derives for a model of 0.2, and for one of 0.1 asked for the proposals, what 0.2 makes rules ([Proposals](#proposals)): a framing family's parts and its beam's lines (Part IV, 6.5.1 and 6.6), a framed wall layer's members, a roof's truss layout and its hip ends, and a floor's joists (7.4), a slab edge element's notches (4.6, rules 12 to 14), a door's and a window's own body (4.3.1), a straight, L or U stair's layout and a cast concrete stair's flights and landings (7.3), a component's frame and the bodies of 7.6, and a room's boundary, ceiling and quantities (7.5).

**The derived layer** (Part IV, 1.1, in the layout and the form 1.1.2 proposes for 0.2, open question 98, which 0.1 leaves open and they meet): `derivedLayer(model, result)` gives one result per element for `.eik/derived/elements/<id>.json`, `{sources, producer, rule, result}`, naming each record its result comes from by its hash. Its keys are `<list>/<id>` (`levels` for the levels), since an id is unique only within its list; a record the result looked for and did not find (a missing host) is named with the hash `null`; and `referrers/<id>` hashes the sorted ids of the elements whose `hostId` or `joins[].to` name the element, so that an opening added to a wall, or a join made from another member's record, makes the wall's or the member's result stale; `categories/<category>` hashes every element of a category, sorted by id, where any of them may change the result (a shaft for a slab, a roof of its group for a gutter). `derivedCurrent(model, file)` says whether each named record still has that hash. A result's `result` holds the element's `bodies`, in the kinds of Part IV, 1.1.1 alone (`derivedBodies`: a level prism an `extrusion`, a sweep carried whole a `swept` with its roll, a cylinder and a rod themselves, and a sloped prism, a sweep with cuts or a taper, a polyhedron and a mesh `faces`, a body for each closed shell), and its `quantities` and a member's `line` where it has them, and nothing else; an element with declared geometry also names the records of its `basis`. The package writes no such file: a model is complete without them, and a result is never in a version (1.1, rule 3). Its file is named by the element's id as a path segment (Part III, 2), so every path stays in `.eik/derived/`. `readDerived(model, files)` reads a folder's layer, the files by their paths: the results a reader may use, by their keys, and a diagnostic for each other file, a warning for one at no result's path (`derived-path`) or not in its form (`derived-form`), an info for one that is stale (`derived-stale`), or, given `own` (`{producer, rule}`), of another producer's rules (1.1.2, rule 5); none of them makes a model invalid.

Diagnostics are `{severity, code, element?, message, rule}`, sorted by element:

| Code | Severity | Rule | What |
|---|---|---|---|
| `id-duplicate` | error | I.6.1 | two elements, types or levels share an id: none of them is used |
| `level-missing` | error | I.4.3 | an element's level is not in the model: it is not compiled |
| `type-missing` | error | I.7.1 | an element's type is not in the model or is of another category: its own width and height are used |
| `profile-invalid` | error | II.6.1 | an element's profile lacks the points its category needs: it is not compiled |
| `size-invalid` | error | II.5.1 | an element's thickness, width or height is not positive: it is not compiled |
| `top-invalid` | error | IV.5.1 | a top constraint names no level or is not above the base: the unconnected height is used |
| `attach-host` | error | IV.5.3.1 | an attached top's host is missing or cannot host it: the unconnected top is used |
| `attach-cover` | error | IV.5.3.1 | an attached top's host does not cover the wall's whole centreline in plan: the unconnected top is used |
| `attach-face` | error | IV.5.3.2 | an attached top names a layer its host does not have: the unconnected top is used |
| `attach-below` | error | IV.5.3.6 | an attached face does not rise above the wall's base, or falls below it by more than the host's depth: the unconnected top is used |
| `attach-outline` | info | IV.5.3.7 | an attached wall's wallOutline is its unconnected outline: the face sets its top, and the outline is not applied |
| `base-host` | error | IV.5.2.1 | an attached column base's host is missing or not a foundation or slab: the recorded base is used |
| `base-cover` | error | IV.5.2.1 | an attached column base's host does not contain the column's centre: the recorded base is used |
| `base-length` | error | IV.5.2.4 | an attached column base leaves no positive length: the recorded base is used |
| `opening-host` | warning | IV.4.3 | a door or window whose host is not a wall cuts nothing |
| `frames-fit` | warning | IV.4.3.1 | in 0.2, or in 0.1 with the proposal frames, a door or window whose family's sizes leave no room for its frame, its leaves or sashes or its glass, or whose wall has no size: it has no body of its own |
| `frames-profile` | error | IV.4.3.1.15 | in 0.2, or in 0.1 with the proposal frames, a door or window whose family gives a profile that is no outline of Part II, 5.2, whose bounding box is not its members' width by their depth, or that shapes a frame that follows its wall: it has no body of its own |
| `component-frame` | warning | IV.7.6 | in 0.2, or in 0.1 with the proposal components, a component that has no frame: axes whose x is zero or whose y runs along x, a profile whose two points are one, a plate's column or its own level missing, or a cast-in item whose `hostId` names no wall with a straight baseline: it has no body |
| `stairs-layout` | warning | IV.7.3.1 | in 0.2, or in 0.1 with the proposal stairs, a straight, L or U stair that cannot be laid out (no risers or going, its top not above its base, a first run of as many risers as it has, an L's landing shallower than its run is wide): it has no layout, and no body |
| `framing-expression` | error | IV.6.6.1 | in 0.2, or in 0.1 with the proposal framing, a beam or a slab edge element whose framing family does not evaluate (an expression that is invalid, a parameter outside its range or named twice, or one the element gives that is not of scope instance): it has no body |
| `framing-part` | error | IV.6.6.1 | in 0.2, or in 0.1 with the proposal framing, a beam or a slab edge element whose framing family's parts cannot be built (a Length, a width, a depth or a plate's thickness outside its range, a member under 1 mm long, an outline that is not simple, a cut through a part's centre, a joint by a part that is no part or is not convex, a tail without its parameters, a part cut away, a plate off its members or on a slab edge element): it has no body |
| `framing-layout-rule` | warning | IV.7.4.1 | in 0.2, or in 0.1 with the proposal framingLayout, a layer's framing rule that cannot be read, or a layer with no thickness: that layer has no layout |
| `framing-layout-top` | warning | IV.7.4.3 | in 0.2, or in 0.1 with the proposal framingLayout, a framed wall whose top these rules cannot take (its attached or cut face not one of Part IV, 5.3, a barrel vault, a top constraint below its base, no envelope, or a top plate with no room under it): it has no layout |
| `framing-layout-floor` | warning | IV.7.4.4 | in 0.2, or in 0.1 with the proposal framingLayout, a framed wall whose framingTop.floorId names no slab: it has no layout |
| `framing-layout-materialised` | info | IV.7.4.12 | in 0.2, or in 0.1 with the proposal framingLayout, a framed wall whose members, or a roof whose trusses, are materialised as elements (Part II, 10): no layout is derived for it |
| `framing-layout-roof` | warning | IV.7.4.13 | in 0.2, or in 0.1 with the proposal framingLayout, a roof whose truss layout cannot be laid out (its family, its faces, its bearings, its range, a truss's span or heel): it has no layout, and its walls are laid out without its trusses |
| `framing-layout-joists` | warning | IV.7.4.15 | in 0.2, or in 0.1 with the proposal framingLayout, a floor whose joist rule cannot be read, or that has no level or outline: it has no joists |
| `framing-layout-note` | info | IV.7.4 | in 0.2, or in 0.1 with the proposal framingLayout, what a layout says of a framed layer, a truss layout or a floor's joists: an opening above its frame, two openings too close for their jacks and kings, a header cut at the plates, a joint outside the frame, a heel zone too low for heel members, a roof opening not framed, bearings out of line, a hip end too short to frame, a hip truss or a jack left out or with no bearing, an edge of a floor not framed |
| `rooms-enclosed` | warning | IV.7.5.3 | in 0.2, or in 0.1 with the proposal rooms, a room placed by a point that lies in no closed face of the walls, columns and room separators on its level: it has no boundary |
| `rooms-shared` | warning | IV.7.5.4 | in 0.2, or in 0.1 with the proposal rooms, a room placed by a point in the face of another room whose id comes first in code-unit order: that room keeps the face, and this one has no boundary |
| `provision-shape` | error | II.6.10.2 | in 0.2, or in 0.1 with the proposal provision, a provision whose frame, size or centre gives no body: it has none |
| `inset-empty` | warning | IV.4.2.2 | a layer's inset leaves nothing of the outline |
| `footing-invalid` | error | IV.4.2.5 | a round footing without a centre or a diameter: it is not compiled |
| `roof-edge` | warning | IV.4.4 | a roof edge rule names no edge of the roof's outline |
| `roof-below-eaves` | warning | IV.4.4.2 | a pitched roof's top surface, the least of its planes, falls below its edges' height inside its outline: a slope-defining edge's plane runs on past a re-entrant corner |
| `trim-invalid` | error | II.6.7 | an outline edge trimmed to a wall's face that breaks a rule of Part II, 6.7 (an edge its outline does not have, or one trimmed twice, or a round footing's; a host that is not in the model or not a wall; an edge not parallel to the wall, or short of its face), or whose face cannot be found (a wall without a baseline or a thickness): it trims nothing |
| `trim-empty` | error | II.6.7.5 | the trims of an outline's edges leave less than 100 mm² of it: none of them is applied |
| `member-line` | error | IV.6.7.1 | in 0.2, or in 0.1 with the proposal member, a member without a line: its profile not two points, its end or its end's level missing, or its two ends less than 10 mm apart: it has no body |
| `slab-plane` | error | IV.4.2.1.7 | in 0.2, or in 0.1 with the proposal slopedSlab, a slab whose plane cannot be read (its gradient or its direction not given once, an edge its outline does not have, a slope over 75°): it has no body |
| `slab-plane-edge` | error | IV.4.2.1.6 | in 0.2, or in 0.1 with the proposal slopedSlab, a slab edge element on a slab whose plane slopes: it has no body, and the slab no thickened edge or chamfer's fill |
| `beam-rise` | error | IV.6.2.2 | a beam with neither a section nor a framing family has an endRise: it is built level |
| `taper-shape` | warning | IV.6.2.4 | a taper on a member that is not a rectangular section is not applied |
| `roll-family` | warning | IV.6.6.15 | a roll on a beam built from a framing family, whose parts it does not turn, is not applied |
| `sheet-section` | warning | IV.6.5.6 | a sheet section's profile is not fixed by Part IV: its area, volume and axis are not derived |
| `section-outline` | error | II.5.2.9 | a custom section's outline breaks a rule of Part II, 5.2, rule 9: its member is not compiled |
| `framing-family` | info | IV.6.2.6 | a beam built from a framing family: its body and lines are not derived here |
| `join-invalid` | error | IV.6.4.5 | a member join that breaks a rule of Part IV, 6.4: it is not applied |
| `join-open` | warning | IV.6.4 | a member join whose cut cannot be found (the members do not meet, or the other has no body here): it is not applied |
| `join-capped` | info | IV.6.4 | a joined end would have to grow further than the reference behaviour lets it to reach its cut |
| `not-derived` | info | IV.2 | a member whose effect on the body is reference behaviour (class B or C), not derived here |
| `category-unknown` | info | IV.1.4 | an element of a category this compiler does not know: no body, or the declared geometry of its own it could place |
| `generic-shape` | error | II.6.6.2 | a generic element without declared geometry, its own or its type's: it has no body |
| `given-shape` | info | VIII.2.3.6 | in 0.2, or in 0.1 with the proposal ifc-read, an element whose shape is given (`shape: "given"`): no body is derived from its members, which place it and nothing more, and it cuts nothing; its body is the one its reader gave |
| `declared` | info | IV.11.1.3 | an element with declared geometry: which results come from the declaration, and which from its category's own rules |
| `declared-frame` | warning | II.12.1 | declared geometry this compiler cannot place (a frame Part II, 12.1 does not give, a line the element does not have here, a body its frame cannot hold): it is kept, not placed |
| `declared-open` | warning | II.12.1.4 | a declared mesh or polyhedron whose faces do not close a surface, one way round: it is placed, and the element's volume is not given |
| `declared-basis` | warning | II.12.1.5 | a record a declaration's basis names has changed since, or is gone: the declaration is placed and used, and unconfirmed |
| `railing-host` | error | II.12.2.2 | a railing whose host is not a stair, a slab, a shaft, a wall or a slab edge element, or has no outline, baseline or level: it has no line |
| `railing-wall-top` | info | IV.11.2.8 | a railing on a wall's top that is attached or has an elevation outline: its line over that top is not derived here (a level top is) |
| `railing-edges` | error | II.12.2.3 | a railing on an outline names no edges, or an edge its host does not have: it has no line |
| `railing-run` | error | II.12.2.2 | a railing's edges do not follow one another round its host's outline as one run: it has no line |
| `railing-type` | error | II.12.2.1 | a railing whose type has no railing payload, or one without a post size or height: its posts are not derived |
| `railing-stair` | info | IV.11.2 | a railing on a stair: its line follows the stair's flights, reference behaviour, not derived here |
| `railing-posts-reference` | info | IV.11.2.6 | a railing without posts.at: its post layout is reference behaviour, and no posts are derived |
| `railing-post-layout` | warning | II.12.2 | a position in posts.at off the railing's line, past its end: no post stands there |
| `edge-host` | error | II.6.1.6 | a slab edge element whose host is not a slab or a foundation with an outline and a level, or whose hostEdgeId names no edge of it: it has no body |
| `edge-section` | error | II.5.6.1 | a slab edge element whose section has no size, or an L's or a U's whose flange or web leaves it no inside: it has no body |
| `edge-family` | info | II.5.4 | a slab edge element whose type is a framing family: its section is its family's, not derived here |
| `edge-chamfer` | warning | II.5.6.2 | a chamfer Part II, 5.6, rule 2 does not allow (on a rectangle, wider than the upstand or higher than its inner face): it is not cut, and its host fills nothing |
| `edge-notches` | warning | IV.4.6.12 | with the proposal adopted, a slab edge element's notches that rule 12 does not allow (no outside upstand, shorter than 10 mm, past their edge, below the flange, shallower than the chamfer, overlapping): they are not cut, and the element is built whole |
| `edge-thickening` | warning | IV.4.6.11 | a slab's thickened edges that do not fit it (the edges moved in by their beams' widths close no simple outline inside it, or a void reaches into them): the slab is built without them |
| `gutter-host` | error | II.6.1.13 | a gutter whose host is not a roof with an outline and a level, or whose hostEdgeId names no edge of it, or whose type gives no section it can be built from (Part II, 5.6, rule 3): it has no body |
| `gutter-buried` | info | IV.4.7.6 | a gutter that other roofs of its group bury along its whole eave line, but for pieces of 10 mm or less: it has no body |
| `shaft-uncut` | info | IV.4.2.8 | a body a shaft meets that this compiler does not cut: a round footing's cylinder, or a chamfer's fill |
| `run-heights` | error | IV.12.1.1 | a pipe or a duct without mep, or whose mep.z does not give a height for each point of its profile: it is not compiled |
| `run-shape` | warning | IV.12.1.4 | a pipe or a duct with two points in a row within 1 mm of each other, or a turn straight back: it is built as Part IV, 12 says, and is no run the reference implementation would write |
| `electrical-profile` | error | XIII.2.3.1 | in 0.2, or in 0.1 with the proposal electrical: an electrical element whose profile is not two distinct points, `at` and `toward`: it has no body |
| `electrical-type` | error | XIII.2.3.2 | in 0.2, or in 0.1 with the proposal electrical: an electrical element whose type has no payload `electrical` with its `mount` and `depth`: it has no body |
| `component-host` | error | II.6.9.6 | a component in a pipe or a duct whose `hostId` names no element of its run's category, or whose run, level or type's `terminal` size is malformed: it has no body |
| `terminal-size` | info | IV.12.6 | a terminal whose type gives no `terminal` width, depth and height, or whose profile gives no point: it has no body but its declared geometry |

## IFC

`ifcOfModel(model, options)` (and `openeik ifc`) prints a model as IFC4: the IFC bridge of Part VIII, 1, written from the specification. An IFC file is a print of a model version, as a PDF is of a drawing: it is never the record and never compared, and it names the version it was printed from. So that EikHub, Femra, Varmra and every other application print IFC the same way, it runs wherever the library runs: it imports nothing of Node.js and reads no clock.

```js
import { readFolder, ifcOfModel } from "@openeik/tools";

const { text, report } = ifcOfModel(readFolder("examples/one-storey-house").model, {
  version: { root: "747068e0…", id: "v-42" },   // named in the print's OpenEik_Source, where given
  at: "2026-10-25T12:00:00Z",                   // the print's time; else the version's `at`; else 1970-01-01T00:00:00Z
  printedBy: { organisation: "EikHub" },        // who printed it; else "OpenEik reference tools"
  crs: { epsg: 3006, verticalDatum: "RH2000" }, // on a map, where asked: an IfcMapConversion to SWEREF 99 TM
});
// report: {schema: "IFC4", products: {total: 10, byClass: {IfcDoor: 1, IfcRoof: 1, IfcSlab: 1, IfcWall: 5, IfcWindow: 2}},
//          openings: {total: 3, …}, types: {total: 6, …}, storeys: 2, spaces: 0, zones: 0, systems: 0, circuits: 0, profiles: 0,
//          ports: 0, portLinks: 0, sourceGlobalIds: 0, map: {epsg: 3006, eastings: 319758.020082, northings: 6400326.036182, …},
//          leftOut: […], approximated: […], …}
```

What it writes:

- **The project, a site and a building**, the site with the project's latitude, longitude, elevation and true north (Part II, 2), and **a storey for each level** at its elevation. Lengths in millimetres, areas in m², volumes in m³, as the reference bridge writes them.
- **Every element of a category of 0.1 that has a body** (VIII.1, rule 1), in its level's storey, from the bodies `compileModel` derives: each body as an `IfcPolygonalFaceSet` of plane faces with their holes, closed where its faces close it (a prism with sloped planes in convex pieces, a member's section carried along its line and cut by its joins), a closed set being one closed shell, every edge met once each way, so that the convex pieces of a sloped prism, which meet face to face, are each a set of their own (buildingSMART's rule GEM001), and a round footing's cylinder as an extruded circle; styled with its layer's or its type's colour.
- **Each element's type**, as the IFC element type of its class, with its material: a material layer set for a type with layers, else the type's material, matched to the model's materials by name (Part II, 4). The type carries its occurrences' predefined type, and a door's or a window's operation, which a typed occurrence leaves unset, as IFC4 has it (buildingSMART's rules OJT001, BLT001 and BLT002); where the occurrences of one type record need it to carry another (two doors of one type that open the two ways, silencers in a round and a rectangular duct), the first keeps the IFC type keyed as the reference bridge keys it (`type:<class>:<type id>`), and each other value has an IFC type of its own (`type:<class>:<type id>:<what it carries, as JSON>`).
- **Openings as voids**: each door's and window's opening an `IfcOpeningElement` through its wall's thickness, the box or the type's outline of Part IV, 4.3, voiding the wall, and the door or window filling it, with its `OverallHeight` and `OverallWidth` and its type (`IfcDoorType`, `IfcWindowType`). The wall's bodies have their openings cut already, as the IFC4 Reference View has them.
- **How a door opens and a window is divided** (`src/ifc-ports.js`), on its type, or on the occurrence where it has none, where the type's family says it (Part II, 5): a door's `OperationType` from its type's `openingFamily` and its own `handFlipped` and `flipped`, its hinge side the opening's start along its wall for `hinge: "left"`, as the reference implementation reads it. Left and right are as IFC4 sees them, looking the way the leaf opens (its "viewed in the direction of the positive y-axis"; Part II, 6.5; Part VIII, 1.2). A door that slides on its wall's face or past its other leaves (`operation: "slide"`) or into the wall (`"pocket"`) `SLIDING_TO_LEFT` where it slides toward its left, its hinge side, and `SLIDING_TO_RIGHT` toward its right, and a pocket door of two leaves `DOUBLE_DOOR_SLIDING`; a door that swings `SINGLE_SWING_LEFT` where its hinges are on the left and `SINGLE_SWING_RIGHT` otherwise, two leaves `DOUBLE_DOOR_SINGLE_SWING`; a garage door `ROLLINGUP`, `DOUBLE_DOOR_FOLDING`, swinging by its `panels`, or `USERDEFINED` `SECTIONAL`; one that does not open, or whose type has no family, `NOTDEFINED`. A window's `PartitioningType` from its lights across and up (an `openingFamily`'s `lightsX` and `lightsY`, or its given mullions; a `windowFamily`'s `panes` and `transoms`): `SINGLE_PANEL`, `DOUBLE_PANEL_VERTICAL` or `TRIPLE_PANEL_VERTICAL` side by side, `DOUBLE_PANEL_HORIZONTAL` or `TRIPLE_PANEL_HORIZONTAL` one above the other, any other grid `USERDEFINED` `<across> x <up>`. A type carries its occurrences' operation, and where they open differently each way has an IFC type of its own (above); the reference bridge writes only a sliding door's, on the occurrence, and `NOTDEFINED` on every type.
- **Stand-ins where the compiler derives none** (`src/ifc-approximate.js`), each said to be approximated in the report, with why: Part IV gives these no body, their shapes being reference behaviour, so the bridge builds a simple one from the records alone, where the reference bridge puts the real one. A door or a window, unless it declares a body: the box of its opening, or its type's outline, through its wall's thickness. A component (`IfcBuildingElementProxy`): a box `width` along its orientation, its type's `component.depth` across and `height` up (down where `componentInverted`), centred on its insertion point. A slab edge element of a framing family (`IfcBuildingElementProxy`): its section's bounding rectangle along its host's edge, hanging from its anchor; every other slab edge element and gutter has the compiler's body (Part IV, 4.6 and 4.7). A gutter on a barrel vault (`IfcPipeSegment` GUTTER): its section along its roof's edge moved out by the edge's overhang and its offsets, its top at the roof's top surface there. A stair: an `IfcStair` of its shape (STRAIGHT_RUN_STAIR, QUARTER_TURN_STAIR, HALF_TURN_STAIR, QUARTER_WINDING_STAIR and HALF_WINDING_STAIR where it turns in winders, SPIRAL_STAIR, CURVED_RUN_STAIR, as the reference bridge writes them; its type NOTDEFINED; with `Pset_StairCommon`), its risers its own count, else the fewest its type's `maxRiser` allows, and its parts aggregated under it, keyed `stair-part:<id>:<piece>` as the reference bridge keys them and placed in the stair's placement (buildingSMART's rule OJP001), each flight with its numbers in `Pset_StairFlightCommon` (IFC4 deprecates `IfcStairFlight`'s own, which are unset: rule IFC102), laid out from the records as the reference implementation lays them: a straight stair's one `IfcStairFlight` (`flight:0`), a solid of its treads down to its base, on its location line; an L's or a U's two flights (`flight:0`, `flight:1`) of `firstRunRisers` and the rest (half, rounded up, where it gives none), each a solid of its treads down to the stair's base, and its landing (`landing:0`, an `IfcSlab` LANDING, `landingDepth` deep, else as deep as the stair is wide, a U's spanning both runs and their `gap`, 100 mm where none is given) from the base up to its top; an L that turns in winders, its two flights and its winders (`winders`, an `IfcStairFlight` WINDER, their going on the walkline at half the width), the box of the turn's square up to the last winder; a spiral or a helical stair, one flight (SPIRAL, CURVED), the sector of a cylinder its treads sweep from its start, its going measured midway between its radii, from its centre for a spiral, between its inner and outer radii for a helical stair (a ring where it turns round more than once), up to its last tread; a stair that turns in balanced winders, or a U in winders round a newel, the box round its runs and its turn, the stair's own body. Its stringers, waist, nosings and a spiral's column are the reference's, and not written. What cannot be stood in is left out, and the report says why: a component placed by `axes` or `insertAt`, a plate, a bolt group or a weld, a void form or an edge detail; a stair with no risers or going, no width, or a first run as long as the stair.
- **Shafts' openings** (Part IV, 4.2, rule 8; Part VIII, 1.3): in each slab, ceiling, roof or, for a stair's shaft, foundation that a shaft cuts and that is written, an `IfcOpeningElement` named by the shaft, keyed `shaft:<shaft id>:<host id>` as the reference bridge keys it, `OPENING` where it takes all of the host within its outline and `RECESS` where it ends inside it, its outline extruded over the heights it cuts and a millimetre past them, voiding the host (`void:shaft:<shaft id>:<host id>`). The host's bodies have the shaft cut already.
- **Rooms as spaces**: a room drawn by hand as an `IfcSpace` (`room:<id>`), aggregated under its storey (`spaces:<level id>`), named by its number, its long name its name, its body its outline from its base up to `room.height`, else its element's height (approximated: the ceiling under the floors and roofs above is reference behaviour); `Qto_SpaceBaseQuantities` (`Height`, `GrossPerimeter`, `NetFloorArea`, `NetCeilingArea`, `NetVolume`), `Pset_SpaceCommon` (`Reference`, its type's name), `Pset_SpaceCoveringRequirements` from its and its type's finishes, and its department and unit as `IfcZone`s. A room placed by a point is left out: its boundary is found from the walls round it, which is reference behaviour (Part IV, 2). With the proposal `rooms` (Part IV, 7.5), every room with a boundary and a ceiling is an `IfcSpace`, its body its volume under its ceiling, with all eight of its base quantities (`NetPerimeter`, `GrossWallArea` and `NetWallArea` among them).
- **Member sections**: a beam's or a column's section on its type as an `IfcMaterialProfileSet` of one `IfcMaterialProfile` named by its designation (`material-profile-rel:<class>:<type id>`): a rectangle, I, T and U as IFC4's own profiles, a rectangle with a `radius` as an `IfcRoundedRectangleProfileDef`, an L and an RHS whose legs or walls are one thickness, a CHS, a round bar, and a `custom` section's outline as an `IfcArbitraryClosedProfileDef` (with voids, an `IfcArbitraryProfileDefWithVoids`) of `IfcIndexedPolyCurve`s, its arcs `IfcArcIndex`es, its points less the middle of its bounding box (Part VIII, 1.4); a `2L`, a `sheet`, an outline that breaks Part II, 5.2, rule 9, or sizes the profile's where rules refuse, none, and the report says so. The occurrences' bodies stay faceted, the profile not placed on them, as in the reference bridge.
- **On a map, where the caller asks** (`crs`, `--crs EPSG:3006`): an `IfcMapConversion` from the model's context to an `IfcProjectedCRS` (`EPSG:<code>`, its name, its geodetic datum, `verticalDatum` where given, `Transverse Mercator`, its zone, in metres): the plan's origin at the site's latitude and longitude, projected by Krüger's series (`src/ifc-geo.js`; it agrees with the reference bridge to the micrometre), the plan's x axis east turned by true north and the grid's convergence, the scale 0.001 from millimetres to metres, and the project's elevation offset as its height. Without `crs` none is written, since 0.1 names no coordinate reference system.
- **The reference bridge's classes and predefined types**, and those of Part VIII, 1.1 for heating, ventilation and plumbing: a terminal by its kind, a silencer ROUND or RECTANGULAR by its duct, a fan in a duct `IfcFan` USERDEFINED `INLINE`; **a component by its type** (VIII.1, its note on components; open questions, 73), from the records alone: a plate (its type's `component.family` `plate`) an `IfcPlate`, a bolt group (its `bolts`, its type's family `bolt` holding a `component.bolt`) an `IfcMechanicalFastener`, ANCHORBOLT for an anchor bolt and a threaded rod with one nut, DOWEL for a rod with none, else BOLT, with its bolt's diameter and its group's `length` where it gives one (`NominalDiameter`, `NominalLength`), and a weld (its `weld`, its type's family `weld`) an `IfcFastener` WELD, their types NOTDEFINED and keyed by their class; any other component an `IfcBuildingElementProxy`, a bolt or a weld type's element without `bolts` or `weld` too; **the systems** of 1.1, one `IfcDistributionSystem` per system and number, named by its code and number (`VS20`: a project's own code in its `standards`, else the Swedish drawing codes the reference implementation gives by default), with its runs and terminals; a run's insulation (`OpenEik_Insulation`: its code and thickness), a terminal's heat (`OpenEik_Heat`: its power, design difference, flow, kv and presetting) and a space heater's power (`Pset_SpaceHeaterTypeCommon.OutputCapacity`); with the proposal electrical, a luminaire's light output from its type (`OpenEik_Light`: luminous flux in lm, colour temperature in K, beam angle and distribution; Part XIII, 10).
- **Ports** (`src/ifc-ports.js`): each terminal's connections, its type's `terminal.connections` and then one for each of a manifold's `outlets`, as `IfcDistributionPort`s nested in it (`IfcRelNests` 'Ports', `ports:<id>`), keyed `port:<terminal id>:<index>`, the index a run's `mep.joins` names; and at each end of a run its record joins to one of them (Part II, 6.9) a port of its own at that connection (`port:<run id>:start` or `:end`), connected run to terminal by an `IfcRelConnectsPorts` (`joins:<run id>:<end>`). Each named by its system's code and its size (`KV 15`), `PIPE` or `DUCT` by whether its system carries air, its system's `IfcDistributionSystemEnum`, placed in its element's placement, and flowing `SOURCEANDSINK` on a heating or a cooling system (Part VIII, 1.1); on another, out of the terminal (`SOURCE`) and into the run's end (`SINK`) where what the system carries leaves the terminal (the waste of the systems that fall by gravity, extract, exhaust and kitchen exhaust air), else the other way, as the reference bridge has it. Part II gives a connection's `at` no frame and an outlet no place: the bridge reads them as the reference implementation does (x across to the left of the way the terminal faces, y along it, z up from its base; outlets in rows across its width at most 60 mm apart, 20 mm in from its sides, the rows evenly through its depth), and the report says the ports are approximated. A component's connections (Part II, 6.9, rule 9), and a join to a connection that is not printed, are left out, and said so.
- **Electrical installations, with the proposal electrical** (`proposals: ["electrical"]`, `--proposal electrical`; Part XIII, 10; `src/ifc-electrical.js`): each device as the class and predefined type of its type's kind (`IfcOutlet` POWEROUTLET, DATAOUTLET, AUDIOVISUALOUTLET or USERDEFINED `LIGHTINGOUTLET`; `IfcSwitchingDevice` TOGGLESWITCH, DIMMERSWITCH, MOMENTARYSWITCH where its type is `momentary`; `IfcSensor` MOVEMENTSENSOR; `IfcLightFixture` DIRECTIONSOURCE, POINTSOURCE, SECURITYLIGHTING or USERDEFINED `LIGHTINGTRACK`; `IfcJunctionBox` POWER; `IfcElectricDistributionBoard` DISTRIBUTIONBOARD or USERDEFINED `METERCABINET`; `IfcElectricAppliance` NOTDEFINED or USERDEFINED `EVCHARGER`; `IfcAudioVisualAppliance` SPEAKER; `IfcCommunicationsAppliance` USERDEFINED `ACCESSPOINT`; the kind `other` the class its type's `ifcClass` names), a cable carrier as an `IfcCableCarrierSegment` of its kind and a modelled cable as an `IfcCableSegment`; the standard property sets IFC has for a device (`Pset_ElectricalDeviceCommon`, its rated current the upper bound of an `IfcPropertyBoundedValue`; `Pset_OutletTypeCommon`; `Pset_SwitchingDeviceTypeCommon` by section 10's switch table, its switch function an `IfcPropertyEnumeratedValue` of IFC4's `PEnum_SwitchFunctionType`; `Pset_LightFixtureTypeCommon`), as IFC4 defines them, `OpenEik_Light`, and `OpenEik_Electrical` for its designation, purpose, power and what it controls, by GlobalId. **The circuits**: one `IfcDistributionSystem` `LV`, ELECTRICAL, aggregating an `IfcDistributionCircuit` for each circuit that has something printed (its designation, its name, and `OpenEik_Circuit`: its board or that it is a supply, phases, voltage, protection and cable), in the natural order of their designations (`G2` before `G10`), its devices and modelled cables its members; its start its board's SOURCE port `Circuit#n` (`IfcRelAssignsToProduct`), n its place among the board's circuits in that order; each leg an `IfcRelConnectsPorts` from that port, or a SOURCE port `Out` of the device its `from` names, to the `Line` SINK port of the device it feeds, through the `Input` and `Output` of each piece of its modelled route; each port an `IfcDistributionPort` CABLE, ELECTRICAL, nested in its element at its point, as the reference bridge keys them (`el-port:<id>:<port>`, `el-circuit:<id>`, `el-leg:<circuit id>:<to>`, `el-system:LV`). Where a device or a board feeds two legs or more, each further leg leaves a SOURCE port of its own (`Out 2`, `Circuit#1 2`): IFC4 connects a port to one port at most, and the reference bridge's prints break that rule there.
- **Reinforcement**: a rebar element as an `IfcReinforcingBar` with its bars' diameter and section, a mesh as an `IfcReinforcingMesh` with its sheets' size and its bars' diameter and section where the record gives them, each with `OpenEik_Reinforcement` (its mark, shape code, grade, count, spacing, face and cover, as given). Their bodies are stand-ins: a bar set (`bars`) a rod of its diameter along each bar's first leg, `dims.a` from its `origin` along its `xAxis`, the bars `spacing` apart along `along` (its shape's other legs come from a national shape code, Part II, 8, and are not written); reinforcement in a layer (`reinforcement`, or a mesh's `sheet`) a sheet two bars thick over its host's layer (its `layerId`; a sheet's `layer`, else its host's structure layer), its cover in from the face it lies at. A bar set in a beam, a column or a slab edge element is placed as one in a slab, in the model's axes. A rebar or a mesh whose host Part II, 6.1, rule 6 does not allow (a rebar in a wall, a mesh in a beam) is left out, as the validator's `host-category` error says it is invalid; a mesh in a wall too, and reinforcement in a layer of a column, where they lie in their host being reference behaviour.
- **Plants, roads and parkings**: a plant as an `IfcGeographicElement` USERDEFINED of its type's kind (`TREE`, `SHRUB`, `HEDGE`), its body a cylinder of its crown up to its height (a hedge, a band as wide as its crown along its line); a road and a parking as an `IfcBuildingElementProxy` USERDEFINED `ROAD` or `PARKING`, as the reference bridge writes them, a band of the road's width along its line its type's thickness deep, and the band a parking's spaces take beside its line; all approximated. A terrain's ground and its gradings are not written.
- **A railing on a stair** as an `IfcRailing` aggregated under its stair after the stair's parts, as the reference bridge aggregates it: its body along its record's own line, rising evenly from the stair's base to its top, as high as the railing and as thick as it is wide (approximated: the line on a stair follows its derived flights, reference behaviour in 0.1).
- **A generic element** as the IFC4 entity its `ifcClass` names, USERDEFINED with what it stands for, else as an `IfcBuildingElementProxy`, and the report says so (VIII.1, rule 4).
- **GlobalIds** derived as the reference bridge derives them (VIII.1, rule 3): an element keeps its GlobalId across versions, alternatives and copies, and a print of this bridge and one of the reference bridge give an element the same GlobalId. An element or a type imported from IFC keeps the GlobalId it was imported with (`ifcSource.globalId`), as a round trip should and the reference bridge does: an element's own, an opening's where it was imported as its `IfcOpeningElement`, a space's where it was imported as its `IfcSpace`; where several records carry one (types imported from one IFC type), the first in the code-unit order of its key, `elements/<id>` or `types/<id>` (an element before a type), of those the print writes an object for keeps it, whatever the order the print writes them in, and the others are derived; so is one another object of the print has already; and the report says so (`sourceGlobalIds` counts those kept).
- **Imported property sets** (`ifcSource.properties`; Part VIII, 1, rule 5, and 1.5; `src/ifc-properties.js`): an element's on the object it was imported as (its opening where it was imported as an `IfcOpeningElement`), else on its product or its space; a type's as each of its IFC types' own sets (`HasPropertySets`). A set IFC4 defines and whose template applies to the object, by its class, its predefined type and whether the set is driven by types or by occurrences, as buildingSMART's rule PSE001 holds it, is written under its name, each property its template defines typed as the template has it: its measure, an enumerated value of its enumeration, a bounded value's set point, a list value of one value; a number in the record's units (millimetres, degrees, the SI unit of any other measure) written in the print's, whose unit assignment declares each measure's SI unit, derived ones such as W/(m²·K) among them. Every other property, of a set IFC4 does not define or apply, one its template lacks, and a value it cannot type, goes into a set named without `Pset_`, or `Imported_` and the set's whole name where that name is empty, starts with `Pset` in any case, with `OpenEik_` or `Imported_`, is one of the bridge's own sets on the object, or is taken by another imported set; a text an `IfcLabel` (an `IfcText` beyond 255 characters), a number an `IfcReal`, a truth value an `IfcBoolean`. An imported set of the name of one the bridge writes itself joins it, the bridge's own values kept and the imported ones left out. The report lists each (`imported properties`). The templates are buildingSMART's table of its validation service, `features/resources/IFC4/pset_definitions.csv` of ifc-gherkin-rules 1.0.2, in `src/ifc4-psets.js` (written by `scripts/ifc4-psets.py`; MIT License, [`NOTICE`](NOTICE)): where it gives a property no data type, or an enumeration no values, the value is one it cannot type.
- **A railing's height** (Part VIII, 1, rule 6): `Pset_RailingCommon.Height` on each `IfcRailing` whose type gives `railing.height`, as an `IfcPositiveLengthMeasure` in millimetres, the height its top rail carries it to (Part IV, 11.2, rule 5), as the reference bridge writes it; an imported `Pset_RailingCommon` joins it, its `Height` left out.
- **An application's own property sets** (`sets`; Part VIII, 1, rule 7; `applicationSets` in `src/ifc-properties.js`), data the model does not hold, keyed `elements/<id>` and `types/<id>`, each `{<set name>: {<property name>: text | number | truth value}}`: an element's on its product and a room's on its space, after the bridge's own sets and the imported ones; a type's as the own sets (`HasPropertySets`) of each IFC type written for it. A text an `IfcLabel` (an `IfcText` beyond 255 characters), a number an `IfcReal`, a truth value an `IfcBoolean`; the sets and their properties in the code-unit order of their names, so that the same sets give the same bytes whatever the order of their members; keyed `app-pset:<record key>:<set name>` for their GlobalIds, a type's `app-pset:type:<its IFC type's key>:<set name>`. A set named with a prefix IFC or the bridge reserves (`Pset` in any case, `Qto_`, `OpenEik_`, `Imported_`), or as a set the bridge writes on that object, is left out, not merged; so is a set with no name or no property, and a property that is no text, finite number or truth value; a key that names no printed object is not used, and is no error; the report lists each (`application property sets`). Name your sets with a prefix of your own, as the reference implementation's `Stomra_Railing`. Sets that are not an object are a usage error; without them, a print is what it was.
- **The version it was printed from**: the property set `OpenEik_Source` on the `IfcProject`, with `VersionRoot` and `VersionId` where they are given, `ProjectId`, `Bridge` (`@openeik/tools 0.2.0-draft`) and `Print` (`IfcBoolean` true), the machine's mark.
- **That it is a print, where people read it**: the sentence `A print of OpenEik version <the first 12 digits of its root>: a copy, like paper. The OpenEik building is the source.` (without a root, `A print of an OpenEik building: …`) as the header's second description and as the `IfcProject`'s description (`printSentence`, `report.print`).
- **Who printed it, and with what**: the one `IfcOwnerHistory`, which every root names, names the organisation that printed it (`printedBy.organisation`, `--printed-by`: EikHub writes `EikHub`, Stomra `Stomra`; by default `OpenEik reference tools`), also in the header's `FILE_NAME`, and no person; its `IfcApplication` is the bridge, `@openeik/tools` and its version, developed by OpenEik.
- **The fidelity report's short form**, so that it travels with the print: an `IfcDocumentInformation` named `Fidelity report` on the `IfcProject` (`IfcRelAssociatesDocument`), its description a few lines (the schema and the bridge; what is written by class; what is left out and approximated; the declared envelopes), and its counts in a third line of the header's description. Both are `report.summary` and the counts of `report`, so they are as deterministic as the rest; the reasons are only in the whole report beside the print.

### Bodies an application gives

An application hands the bridge the bodies the specification does not derive yet, so that it prints its IFC with the bridge rather than with a writer of its own, until the specification derives them all (the steward's decision of 26 October 2026). They come as results of the derived layer (Part IV, 1.1), in the form of 0.2 (Part IV, 1.1.1; `schemas/0.2/derived/bodies.schema.json`), keyed by the record each is of:

```js
import { hashValue, ifcOfModel } from "@openeik/tools";

const door = model.elements.find((e) => e.id === "door-1");
const bodies = {
  "elements/door-1": {
    sources: { "elements/door-1": hashValue(door) },          // used only while these hashes hold (Part IV, 1.1, rule 2)
    producer: "My app 2.1", rule: "door-bodies",
    result: { bodies: [
      { kind: "extrusion", profile: [[0, -50], [1000, -50], [1000, 50], [0, 50]], holes: [[[60, 45], [940, 45], [940, -45], [60, -45]]], z0: 0, height: 2100, part: "frame", material: "Timber" },
      { kind: "faces", vertices: [/* … */], faces: [{ outer: [0, 3, 2, 1] } /* … */], part: "leaf" },
    ] },
  },
  "types/bracket": { /* a type's representation, which elements place with {kind: "mapped", origin: [x, y, z]} */ },
};
const { text, report } = ifcOfModel(model, { bodies });
// report.given: [{what: "door bodies", count: 1, elements: ["door-1"], reason: "given by My app 2.1 (door-bodies)"}]
```

The kinds: `extrusion` (a plan polygon less its holes, from `z0` up by `height`), `faces` (plane faces with their holes, and normals where smooth), `rod`, `cylinder`, `swept` (a section of Part II, 5.2 or a polygon, along a line, in a member's frame, with its `roll`) and `mapped` (a type's representation placed). The tags: `material`, `layer`, `part` (a door's or a window's frame, sash, glass, leaf …), `piece` (a stair's `flight:0`, `landing:0`, `stringer:0:left` …), `member` (`{id, class, predefined, objectType, name, section}`, a framing member), `color` and `glass`. In millimetres, z up, in the model's coordinates; a type's in its own frame.

- **Where it is used.** Where the compiler derives no body, and over the bridge's own stand-in, which the report then no longer calls approximated. Over a compiled body where the compiler reports some of it not derived (`not-derived`, Part IV, 2: a wall cut to a roof or with its type's `wallAttach`, a roof's windows, join group, square-cut edges or layer extents, a column's or a beam's connection, recesses and the like), and where a wall's end meets another wall that its result names among its sources, by its record or `categories/wall` (Part IV, 1.1, rule 1), whose join the compiler does not derive (7.1: an end its `joinEnds` leaves free within twice the thicker wall's thickness of another's free end, their bases at one height, or within half the other's thickness and 50 mm of its axis, part-way along it, at more than 10°); the report says it is written over the compiled body, and why (`given`). Elsewhere the compiled body is written, which the rules of class A derive whole. And only while every record its result names has the hash it recorded. A result of a record the model does not have, one no longer current and one given over a compiled body that is written are not used, and the report lists them (`leftOut`, `given bodies`), with why. A result that breaks the schema, an index past its vertices, normals not one a vertex, a key that names no record, and a member given by two elements are refused (`ifc-bodies`), naming where.
- **The forms.** An extrusion, a rod, a cylinder and a swept section an `IfcExtrudedAreaSolid`: of an `IfcArbitraryClosedProfileDef` (with voids, `IfcArbitraryProfileDefWithVoids`), an `IfcCircleProfileDef`, or the section's IFC4 profile (`IfcLShapeProfileDef` …, its outline where IFC4 has none, a 2L's two angles as an `IfcCompositeProfileDef`), placed by its axis and its reference direction both where they are not the storey's. Faces an `IfcPolygonalFaceSet`, closed where every edge is met once each way; an `IfcTriangulatedFaceSet`, with its normals, where they come in triangles or a face is not plane (each face in triangles of its own points, its holes bridged; the report says how many were not plane). A type's representation an `IfcRepresentationMap` of its IFC type, written once; each element that places it an `IfcMappedItem`. One representation holds one kind of item (IFC4's rule CorrectItemsForType): an element's bodies are swept solids only where all of them are, else faces, an extrusion then the faces of its prism, a circle a polygon, said so.
- **Pieces, members and parts.** A piece is a part of its element, aggregated under it and placed in its placement, keyed as the reference bridge keys a stair's (`stair-part:<id>:<piece>`; another element's `piece:<id>:<piece>`): a flight or the winders an `IfcStairFlight`, with the numbers the bridge works out for the stair; a landing an `IfcSlab` LANDING; a stringer an `IfcMember` STRINGER; a landing beam an `IfcBeam` LANDING_BEAM; a bearing an `IfcDiscreteAccessory` BEARING_PAD; a newel or a column an `IfcMember` POST. A member is an element of its `class` and `predefined` type (a predefined type the class does not have written USERDEFINED, said so), keyed `framing:<member id>`, aggregated under its element (`framing-aggregates:<id>`), with its material; members of one class, predefined type and `section` share an IFC type (`type:<class>:framing:<predefined>:<user type>:<section>`), as the reference bridge writes them. An element's parts (`part`, on the bodies of any element), where its bodies name two or more, make its material an `IfcMaterialConstituentSet`, a constituent a part with the part's material, and each part an `IfcShapeAspect` of its bodies, as a type's representation's parts make its type's; one part, or none, adds nothing, and the element keeps its type's material (Part IV, 1.1.1; open question 91).
- **The same bytes.** Without `bodies`, or with none, a print is what it was, byte for byte. With them, the same results give the same bytes, whatever the order of their keys: elements by id, and each element's bodies in their given order.

Against the reference implementation's own print (Stomra, its work 727, `eikBodies`, read and run apart, 26 October 2026): for its garage example, with its bodies given and its framing members among them, both prints have the same 493 objects, every GlobalId, class, predefined type, property set and quantity the same (to 0.05, `OpenEik_*` and its own sets left out, with `tools/ifc-validate.mjs diff`), and the bridge's passes IfcOpenShell 0.8.4's validation with no issue. On its extension, its balcony and its steel frame, both prints have the same 131, 181 and 668 objects, every GlobalId, class and predefined type the same, since a component takes its class by its type (above; the steward's word of 26 October 2026, "steel plates and bolts should be plates and fasteners"): 3 plates and their type, 6 and theirs, and 312 plates, 151 bolt groups (114 BOLT, 31 ANCHORBOLT, 6 DOWEL) and their 63 types; every plate's and bolt group's attributes are the reference's too, its bolts' diameter and length among them; and the bridge's prints pass with no issue.

**buildingSMART's rules.** Since 27 October 2026 the prints pass buildingSMART's normative rules, those of its validation service (release 1.0.2 of its ifc-gherkin-rules, run with IfcOpenShell 0.8.5 by `tools/ifc-validate.mjs --gherkin`): those of the examples, the conformance suite's installation, the electrical fixture and the test houses with bodies given, and of the reference implementation's garage, extension, balcony, steel frame, installations and villa with a garage and a guesthouse, with their bodies given and without. Two rules of that release stop on errors of their own, whatever the file, and the suite lists them apart, as rule errors: PSE001 on a property set its table defines in Python's notation (`Pset_StairCommon` and `Pset_ElectricalDeviceCommon` among them), GRP001 on a group that is not an `IfcGroup` itself. A third, TAS001, stops on a face with voids whose voids have different numbers of points, which it reads into one array, as a given wall's face with two openings of different shapes has.

**The same bytes for one version.** The header's time stamp and the owner history's creation date are the time `at` gives, else the version's `at`, else `IFC_FIXED_TIME`. Nothing else depends on when, where or in what order the model was read: the same model, options and bridge give the same bytes, and another time changes only those two lines.

**The fidelity report** (VIII.1, rule 2) beside every print, and its short form in it (`summary`): the schema; who printed it (`printedBy`) and the sentence that says it is a print (`print`); the counts of products, openings and types by class; the storeys, spaces, zones, systems (the electrical `LV` among them), circuits and profiled member types; the ports and the joins between them (`ports`, `portLinks`); the GlobalIds kept from an IFC imported (`sourceGlobalIds`); the map conversion, where one is written (`map`); what is left out (`leftOut`) and what is approximated (`approximated`), each with the elements and the reason; which bodies are declared envelopes, and at which level (`declared`, Part IV, 11.1, rule 6); and the compiler's errors and warnings. What it approximates are the stand-ins above. What it leaves out: a room placed by a point, a stair or a component it cannot stand in, the other reference behaviour of Part IV, 2 and 7 with no stand-in (wall joins: walls end square; the fittings between runs, so runs that meet are not connected port to port, and a run's end its record joins to no terminal has no port; a terrain's ground and its gradings); a rebar or a mesh on a host Part II does not allow; a space's net perimeter and wall areas, which rest on the walls round it; a leg's route where no cable is modelled for it (its ports are connected, with no `IfcCableSegment`); a wired control's wiring (what a switch works is in `OpenEik_Electrical.Controls`); and a map conversion unless `crs` names a system. Circles are polygons of 32 sides, where a footing's cylinder is not written whole.

Against the reference bridge (Stomra `main` `cc93660b`, 26 October 2026): for `examples/one-storey-house` both write 10 products, 3 openings and 6 types, every GlobalId, class and predefined type the same. For `examples/l-house` both write 130 products, 13 openings and 18 types, with the same GlobalIds and classes (the components and slab edges `IfcBuildingElementProxy`, the gutters `IfcPipeSegment` GUTTER), and five `IfcMaterialProfileSet`s; the stand-ins lie where the reference's bodies do, the components' boxes to the millimetre, but for the doors and windows (the reference writes their frames, thinner than the wall); the slab edges, compiled by Part IV, 4.6, are the reference's to the millimetre, mitred ends included, and the gutters' boxes too but where the reference, sampling at 25 points, runs a gutter on to the first point another roof of its group buries (Part IV, 4.7, rule 6; Conformance, 3). With a straight stair and two rooms added to the house, both write the stair, its flight and its type, the drawn room's space and its department's zone, with the same GlobalIds and bodies; the reference also writes the room placed by a point, its boundary found from the walls; and with `crs: {epsg: 3006}` the same map conversion to six decimals. For a building with every pipe, duct and fixture type of the reference's library (112 products, 108 types, 17 systems), both write the same products, types and systems, with the same GlobalIds, classes, predefined types, names and members, and since 26 October 2026 the same 118 ports, each with the same name, flow direction, type, system and point, nested alike (Stomra `main` `bd839668`). With an L stair on a landing, a U, an L in winders, a spiral and a helical stair, four doors and a window with families of their own, and a basin with two pipes joined to it added to the house, both write the same 71 objects with the same GlobalIds and classes: every stair, flight, winders and landing, each flight's numbers, the five ports and their two connections; the parts' plans lie where the reference's do (a flight's and a landing's solid reaching down to the stair's base, where the reference's waist and landing slab do not), and a stair in balanced winders fills the box round the reference's parts exactly. The reference also writes a helical stair's stringers and a spiral's column. The doors that slide open as the reference writes them where they open to the wall's exterior; where they open to its interior, the reference writes the other side, since it does not look the way the leaf opens (Part VIII, 1.2); the reference writes a swinging door, and every type, `NOTDEFINED`. On the four example projects of the reference implementation's own comparison (Stomra `main` `0c8fcea1`, 26 October 2026), every object both write has the same GlobalId and class, but for one of two slab types imported with one GlobalId, which the order of writing decided until 28 October 2026 (since then the first by its id keeps it): an example villa imported from IFC, 199 objects with the GlobalIds it was imported with (100 of 194 the same before); its installations with the proposal electrical, 808 objects, every device and its type, the 36 circuits, the `LV` system, the reference's 207 ports (and 19 more where a device feeds two legs) and the four railings on its two stairs; a villa with a garage and a guesthouse, 664 objects, 89 bar sets, its meshes, its 12 plants, its road and parking; and a garage as before. What the reference writes there and this bridge does not: the framing members of framing families, the rooms placed by a point, a terrain's ground, and 23 bar sets an edge beam hosts, which Part II, 6.1, rule 6 does not allow. The prints of these buildings pass IfcOpenShell 0.8.4's validation against the IFC4 schema, its where rules included (checks run by hand on 26 October 2026; IfcOpenShell is no dependency); the reference's print of the installations does not: 86 issues, 14 of them a port connected to two ports where a device or a board feeds two legs.

### Reading IFC

`eikOfIfc(text, options)` reads an IFC file into OpenEik once, to start a project: Part VIII, 2, of 0.2 (in 0.1 the proposal `ifc-read`), by the steward's decision of 29 October 2026 (open questions, 95). It is the bridge the other way, and as the bridge it imports nothing of Node.js, reads no clock and has no dependency: the same file gives the same bytes. It is no live link to the file, and it tracks no revisions between deliveries; an assistant reviews what it read, and upgrades it.

```js
import { checkModel, eikOfIfc, ifcOfModel } from "@openeik/tools";

const { model, bodies, report } = await eikOfIfc(text, { mesher });   // mesher: optional, for what it does not read
checkModel(model, { proposals: ["ifc-read"] }).ok;                     // true for the examples' prints read back
const again = ifcOfModel(model, { bodies, proposals: ["ifc-read"] }); // the same elements and types, GlobalId by GlobalId
// report: {schema: "IFC4", view, units: {length, millimetres, angle, degrees}, project, levels, materials,
//   elements: {total, byCategory}, types: {total, byCategory, own}, classes: {IfcWall: {count, category, core, mesher, none}, …},
//   bodies: {core, mesher, none, types}, properties: {sets, values}, mesher, approximated, leftOut, withoutBody, summary}
```

- **What it writes.** The project, its id the IfcProject's GlobalId's; a level for each storey; an element for each element of the file (an IfcElement but feature and virtual elements) and each IfcSpace, of the category its class maps to (`IFC_READ_CLASSES`: a wall, a slab, a roof, a foundation, a ceiling, a beam, a column, a door or a window in a wall's opening, a railing, a pipe, a duct, furniture, a room), else `generic` with its class; a type for each IFC type; materials by name. Ids are GlobalIds as UUIDs (`idOfGlobalId`), records in code-unit order of their ids.
- **What it keeps.** Each object's GlobalId, class, predefined type and user-defined type, in `ifcSource`; its property sets in this specification's units (`units: "specification"`), converted by the file's IfcUnitAssignment, SI prefixes, conversion-based units such as feet and degrees, and derived units.
- **Shapes.** Each product's shape a body given in the derived layer, never its record's geometry: its record carries `shape: "given"`, and `profile`, `offset`, `width` and `height` place it and say nothing of its shape (Part VIII, 2.3, rule 2). It meshes itself an IfcExtrudedAreaSolid of a rectangle, a rounded rectangle, a circle, a hollow circle or rectangle, an ellipse, an I, L, U, T or C shape, an arbitrary profile with or without voids (polylines, indexed poly curves, composite curves of polylines and trimmed lines and circles) and composite and derived profiles; IfcPolygonalFaceSet, IfcTriangulatedFaceSet, IfcFacetedBrep, IfcShellBasedSurfaceModel and IfcFaceBasedSurfaceModel; placed by IfcLocalPlacement chains and IfcMappedItem; an upright extrusion an `extrusion`, a circle's a `cylinder`, a circle swept square to itself a `rod`, a body that is its type's representation placed a `mapped` body of the type's result (`types/<id>`), any other `faces`; arcs in chords, four a quarter turn.
- **The mesher.** A boolean result, the voids of a file that is no Reference View, and every other item go to `options.mesher`, `(ifcText) => Promise<Map<GlobalId, {positions: Float64Array, indices: Uint32Array, color?, part?}[]>>`, positions in millimetres in the file's world, which a separate package builds on web-ifc; it is asked once, and only where it is needed, and its meshes are written as `faces` in triangles, the rule `ifc-read, mesher`. Without it, a difference is written as its first operand and a voided body as the file draws it, said to be approximated, and a product whose shape holds an item it does not read is kept without a body, and named in `report.withoutBody`. It never guesses a shape.
- **Refused** (`ifc-read`): a text that is no ISO 10303-21, a schema other than IFC4 and IFC2X3, a file without one IfcProject, a mesher's answer that is no Map.

Read back and printed again with the proposal, the prints of `examples/one-storey-house` and `examples/l-house` give every element and type again, each with the same GlobalId, class, predefined type and property sets, and each body the volume the compiler gives it (`test/ifc-read.test.mjs`); the project, its site, building and storeys, and the openings differ, since 0.1 holds no GlobalId for them.

### IFCX, experimental

`ifcxOfModel(model, options)` (and `openeik ifcx`) prints a model as IFCX, buildingSMART's JSON form of what was called IFC 5, **experimentally**: it writes buildingSMART's alpha, `ifcx_alpha`, which will change with its post-alpha format, so it is proof that OpenEik maps to IFCX, not a promise of a format, and no conformance class covers it (Part VIII, 1, its note on IFCX; open questions, 86, the steward's decision of 27 October 2026). Like the IFC4 print it is a print of a version, never the record.

```js
import { readFolder, ifcxOfModel } from "@openeik/tools";

const { text, report } = ifcxOfModel(readFolder("examples/one-storey-house").model, { version: { root: "747068e0…" } });
// report: {schema: "ifcx_alpha", from: "IFC4", products: {total: 10, …}, openings: {total: 3, …}, types: {total: 6, …},
//          nodes: 55, meshes: 31, triangles: 968, leftOut: […], approximated: […], lost: […], summary: "…"}
```

It takes `ifcOfModel`'s options and reads the model once, through the IFC4 writer: it prints the model as IFC4 and reads that print back (`readStep`), so that both prints have the same objects, GlobalIds, classes, predefined types, property sets and bodies, and the IFC4 print stays what it is, byte for byte. What it writes, as buildingSMART's hello-wall example writes it:

- **The file**: `header` {id `<project id>@<root>.ifcx`, `ifcxVersion` `ifcx_alpha`, `dataVersion` the version's root, `author` who printed it, `timestamp` the IFC4 print's time, so no clock}; `imports` of buildingSMART's published schemas (`ifc@v5a`, `prop@v5a` and `usd@v1` at ifcx.dev, `IFCX_IMPORTS`); `schemas`, one of the print's own for every key they do not define; and `data`, one entry per node, `{path, children?, inherits?, attributes?}`, one line each.
- **Paths**: an object's GlobalId as the UUID its 128 bits are, as buildingSMART's converter keeps it (`2JUHrTM_j3UxZiBnyBfByx` is `93791d5d-5beb-437b-b8ec-2f1f0ba4bf3b`); a body, which has none, the version-5 UUID of its object's and its name.
- **The tree**, as the IFC4 print relates its objects: the project, its site, building and storeys; under a storey its spaces and elements; under an element its parts, its openings and its ports. An element inherits its type's node (`inherits: {type}`), so that it takes, key by key, what the IFC4 print has its type carry: its predefined type, a door's operation, a window's partitioning, its type's name. The systems and circuits under the project; their members name them (`bsi::ifc::system::partofsystem`), and a port the ports it connects to and its flow (`connectsto`, `flowdirection`).
- **Each node**: its class, `bsi::ifc::class` {code, uri}, the URI that of the class in bSDD's IFC 4.3 dictionary, as buildingSMART's examples point to it; its name and description (`bsi::ifc::prop::Name`, a type's `TypeName`, `Description`); and the IFC4 attributes the published schemas give no key, `openeik::ifc::<attribute>`: `globalId`, `tag` (the OpenEik record's id), `objectType`, `predefinedType`, `elementType`, `operationType`, `partitioningType`, a door's and a window's `overallHeight` and `overallWidth`, a storey's `elevation`, the site's `refLatitude` and `refLongitude` in degrees.
- **Properties and quantities flat**: `bsi::ifc::prop::<Name>`, the bridge's own sets (`OpenEik_*`, no buildingSMART sets) `openeik::prop::<Name>`, each value in IFCX's units, a length in metres, with its quantity kind. IFCX has no property sets: their names are lost, and a property named as another of its object's, in another set and with another value, is not written.
- **Bodies**: each element's, opening's and space's body as `usd::usdgeom::mesh` {points, faceVertexIndices}, triangles whose face has points of its own, a child `Body` of their object, one for each colour (`Body_001` …), at most 65535 points, with its colour and opacity (`bsi::ifc::presentation::diffuseColor`, `opacity`); an opening's hidden (`usd::usdgeom::visibility`), as buildingSMART's examples hide a void. A swept solid is meshed, a circle as a polygon of 32 sides; a type's representation placed by its occurrences is meshed into each. **Placements** as `usd::xformop` {transform}, a 4×4 matrix in the parent node, its rows the axes and the origin, as OpenUSD writes them, where it is not the identity. Lengths in metres.
- **The print's own record**, on the project: `openeik::source`, the fields of the IFC4 print's `OpenEik_Source` (`VersionRoot`, `VersionId`, `ProjectId`, `Bridge`, `Print`), and `openeik::print`: the sentence that says it is a print, who printed it, that it is experimental, and the short form of its report.

**The fidelity report** counts again what the print writes (products, openings and types by class; storeys, spaces, systems, circuits, ports; nodes, meshes and triangles), gives what the bridge leaves out, approximates and is given as the IFC4 print reports it (`leftOut`, `approximated`, `given`, `declared`, `diagnostics`), and adds what IFCX loses of the IFC4 print, and why (`lost`): property-set names; materials, layer sets, profile sets and constituent sets (`bsi::ifc::material` names one bSDD class, with no layers or thicknesses); which door or window fills an opening; zones; where a circuit starts; shape aspects; a bounded value's bounds; the map conversion and true north; a relation's own GlobalId; and any class of the IFC4 print it does not read.

**Checked**, by hand on 27 October 2026, on this machine only: the prints of `examples/one-storey-house` and `examples/l-house` (10 products and 6 types; 130 products and 18 types), and of the house with bodies of every kind given, the conformance suite's installation and the electrical fixture, compose and pass the schema validation of buildingSMART's reference library (`ifcx-core` and its command line, `compose`, from a clone of its IFC5 development repository), and IFClite (`@ifc-lite/ifcx` 4.5.1) parses them with every product and type. For the two examples, each product's box in the IFCX print agrees with IfcOpenShell 0.8.5's geometry of the IFC4 print to a nanometre, and its volume with the compiler's to a millionth.

## Hashes and the reference implementation

A record's object is its canonical JSON (RFC 8785); a kept file's is its text as a JSON string; a folder's is `{"tree": [[name, "t" | "b", hash], …]}` sorted by name, split into `~xx` shards past 4096 entries; a version is a commit object (Part VI). By default `eik.json` is a leaf of the root, as Part VI, 2.2 requires. `versionTree(folder, {eikJson: false})` (`openeik hash --no-eik-json`) builds a tree without it, as versions saved before 11 October 2026 were built.

`test/fixtures/roots.json` holds roots the reference implementation computed with its own code at commit `88f0b375` (`scripts/reference-roots.mts`), whose trees had no `eik.json`, and the tests hold this package's trees without `eik.json` to them: both examples, a folder with kept files of every kind beside the records, and one with 5000 records in one folder (sharded). Exported folders with the reference implementation's whole library (295, 435 and 398 files) gave equal roots too. The fixture also records the roots with `eik.json`, which this package computes and which the reference implementation gave for the same four folders at `09ead506`.

Since 14 October 2026 the names of files are escaped as Part III, 2 says: the L-house's two gutters, whose ids hold colons, are named `study-roof%3Agutter%3Astudy-roof%3Aedge%3A0.json` and `…%3A2.json`. The reference implementation follows the rule since Stomra 506 (`a34cc1a1`), and at that commit its own code gave the roots with `eik.json` of all four folders, the renamed L-house's among them, equal to this package's (`referenceWithEikJson`). The tests hold the L-house as it was before, its two files named with colons, to the root the reference implementation computed at `88f0b375`, and read it as the same model.

## Conformance

The specification's [Conformance](../../spec/09-conformance.md) defines twelve classes for OpenEik 0.2: the reader, the validator, the writer, the compiler (core), the command processor, the version store, the agent host and the bridge, and the four that Parts XIV and XVIII add, the drawing validator, the drawing issuer, the host and the authoring tool (1). A claim to a class of 0.2 is a claim to what Parts XIII to XVIII ask of it too (1.1); a claim to 0.1 is a claim to the eight classes of 0.1 as the tag `v0.1.0` has them, without Parts XIII to XVIII. An implementation claims a class only when it meets every requirement the class lists. This package claims the **writer** and **compiler (core)** classes of OpenEik 0.2.0-draft, and of OpenEik 0.1.

| Class | This package | Tested in `test/` | Not met |
|---|---|---|---|
| Writer | claims it of 0.1 and of 0.2: `modelFiles`, `writeFolder`. A model of 0.2 is written by 0.2's rules, in the owners' layout (Part III, 4.4), its circuits and people as records at their paths; a model of 0.1 written as 0.2 is migrated (Part I, 9.8; [Versions](#versions)). An asset of a kind it does not know it never writes: a clone or a pull keeps its pointer and writes no file of it, and the next push writes the pointer back (Part XV, 4, rule 3; `later` in `.eik/remotes.json`). Measured on 30 October 2026: both examples of 0.2 read and written back change no file, byte for byte (Conformance, 2), and every test of the column passes | `write.test.mjs`, `canonical.test.mjs`, `tree.test.mjs`; of 0.2, `versions.test.mjs`, `owner-folders.test.mjs`, `migrate-model.test.mjs`, `electrical.test.mjs`, `drawings.test.mjs`, `assets.test.mjs` | |
| Compiler (core) | claims it of 0.1 and of 0.2: `compileModel`. For a model of 0.2 it derives the rules 0.2 adds to Part IV, 4 and 6 too, the class-A rules of Part IV, 7.3 to 7.6 (erratum 0.2-E1), and Part XIII's bodies (Conformance, 1 and 1.1; [Compiling](#compiling)) | `compile.test.mjs`, `hosted.test.mjs`, `trims.test.mjs`, `runs.test.mjs`, `derived.test.mjs`; of 0.2, `sloped-slab.test.mjs`, `frames.test.mjs`, `adopted.test.mjs`, `framing.test.mjs`, `framing-lines.test.mjs`, `member.test.mjs`, `stairs.test.mjs`, `framing-layout.test.mjs`, `framing-roofs.test.mjs`, `rooms.test.mjs`, `components.test.mjs`, `electrical.test.mjs`, `versions.test.mjs` | |
| Reader | implements it: `readFolder`, `readVersion`, `readModel` on the single-file form, each with the whole-model check, or without it (`check: false`), refusing only what Part III, 12 lists (a file that is not UTF-8 among it), and, with or without it, a file that is not I-JSON (Security and privacy, 7.3, rule 1). It keeps no member of `project/order.json` that names no record list, with a warning, as Part III, 9, rule 5 allows. A model of 0.2 it reads by 0.2's rules: its circuits and people as records, a record in either layout (Part III, 4.4), `project/owners.json`, and the files of `assets/` as bytes | `read.test.mjs`, `check.test.mjs`, `store.test.mjs`, `ijson.test.mjs`; of 0.2, `versions.test.mjs`, `owner-folders.test.mjs`, `drawings.test.mjs`, `assets.test.mjs`, `conformance.test.mjs` | its check of the whole model (Part III, 12, rule 5) leaves out the two rules below |
| Validator | implements it: `checkFolder`, `checkModel`, `openeik check`. A model of 0.2 it checks by every rule of 0.2, Parts XIII to XVIII among them, with no proposal: every case of the conformance suite of 0.2 passes | `check.test.mjs`, `rules.test.mjs`, `validator.test.mjs`, `conformance.test.mjs` | it does not check that a sheet section's sizes follow its profiled sheet (Part II, 5.2); and it checks a framing family's expressions and parameters (Part II, 5.4) only in a model of 0.2, or of 0.1 asked for the proposal `framing`, and there only in a family a beam or a slab edge element is built from (Part IV, 6.6, rule 1), not in one no element uses |
| Command processor | implements it for the core record operations, and for a model of 0.2 `view.upsert` (Part V, 2.2) and Part XIII, 9's kinds, which the command schemas of 0.2 list: `applyCommands`, `openeik apply` | `commands.test.mjs` | the whole-model check after every command leaves out the same two rules |
| Version store | implements Part VI's objects and trees, diff and merge, and push, pull and a clone of one ref into a folder, as a client of a store; of 0.2, a version's assets as pointers, their bytes sent before a push moves its ref and fetched and checked before a clone or a pull writes anything (Part XV, 3 and 6) | `tree.test.mjs`, `diff.test.mjs`, `merge.test.mjs`, `store.test.mjs`, `remote.test.mjs`; of 0.2, `assets.test.mjs` | it keeps no versions of its own: the store does |
| Bridge (IFC) | implements it: `ifcOfModel`, `openeik ifc`, IFC4 (Part VIII, 1, rules 1 to 4, and Part IV, 11.1, rule 6: the fidelity report names the declared envelopes); of 0.2, a print names the version it was printed from, the bridge and who printed it, and says that it is a print (Part XVIII, 7.2) | `ifc.test.mjs` | |
| Bridge (IFCX) | experimental, and no conformance class covers it (open questions, 86): `ifcxOfModel`, `openeik ifcx`, buildingSMART's `ifcx_alpha`, from the IFC4 print | `ifcx.test.mjs` | it follows buildingSMART's alpha, which will change |
| Agent host | not implemented | | |
| Drawing validator | implements it, for a model of 0.2 and for one of 0.1 asked for the proposals `drawings` and `review`: the validator above; freshness (Part XIV, 9.4), `drawingStatus` and `openeik drawing status`; sign-offs verified (14.3), `verifyDrawingSignoffs` and `openeik drawing verify`, the legal tier reported as not verified; a round's anchors where it has the version reviewed (Part XVIII, 2.5, rule 4), `checkReviewAnchors` and `openeik review anchors`; statements, receipts and acceptances verified (Part XVIII, 4.3, rule 6; 5.3, rule 7; 6.2, rule 7), `verifyReviewStatements`, `verifyReceipts` and `openeik review verify` | `drawings-freshness.test.mjs`, `drawings-verify.test.mjs`, `review-anchors.test.mjs`, `review-verify.test.mjs`, `conformance.test.mjs` | everything a validator must, which the validator does not meet (above); it records the SHA-256 of the file of a host's keys that verified a receipt, and leaves comparing a pinned file with a later one (Part XVIII, 5.6, rule 6) to its caller |
| Drawing issuer | not implemented: it checks, verifies and compares issued drawings, and issues none | | |
| Host | not a host: a client of one, through a store's API ([Remotes](#remotes-clone-push-pull)) | | |
| Authoring tool | not one: it saves no receipt or statement a host offers, and records no version of what it issues (Part XVIII, 9) | | |

For a model of 0.2 the compiler derives a framing family's parts and its beam's lines (Part IV, 6.5.1 and 6.6), which 0.2 makes class-A rules, and the class-A rules of Part IV, 7.3 to 7.6, which the class asks of 0.2 too since erratum 0.2-E1: on a model of 0.2, with no proposal, it gives the stairs, framing layouts, rooms and component bodies of the fixtures below that it gives their models of 0.1 asked for the proposals (`versions.test.mjs`). In a model of 0.1 they are reference behaviour, which the compiler (core) class of 0.1 does not require (Part IV, 2; Part II, 5.4 binds only an implementation that derives framing bodies), and it derives them only when asked for the proposals that make them rules of 0.2 (`framing`, `stairs`, `framingLayout`, `rooms`, `components`). The conformance vectors of Part IV, 10 are not published, so the compiler is tested against the rules of Part IV, the quantities it works out in Part IV, 9, and the reference implementation's own results where `test/fixtures/` holds them: bodies (`*-bodies.json`, `notched-edges.json`), stairs, framing layouts and rooms.

## Tests

```sh
npm test                                   # at the repository's root: node --test packages/openeik/test/*.test.mjs
EIKHUB_DIR=<eikhub-checkout> npm test      # also against EikHub's own Worker, in memory, through its test harness
OPENEIK_PLUMBING_TEST=<folder> npm test    # also the IFC print of a folder with every pipe, duct and fixture type
```

The tests cover RFC 8785's number samples and examples, planted errors and their codes, each rule that needs geometry or the types' sizes (valid, broken each way and at its boundary) and the geometry under them, the model-quality rules on a synthetic model (each rule by name, by type name, by IFC class and by shape, and the cases that must not be flagged), the validator against Ajv on more than 11 000 values (Ajv is a development dependency of the repository, not of the package), the roots above, diffs, result manifests, a store in memory that speaks API v0 (its limits included), remotes against it (clone, push, pull, their refusals and conflicts), the command, pipe and duct bodies against the reference implementation's own `runSolids` (`test/fixtures/run-bodies.json`, written by `node tools/run-bodies-reference.mjs <reference-checkout>` at the root), the IFC bridge (`test/ifc.test.mjs`: the examples, the installation of Part II, 6.9 with ducts and duct parts, GlobalIds pinned to the reference bridge's, the stand-ins where the reference bridge puts its bodies, stairs, rooms as spaces, member profiles, the map conversion against the reference bridge's and the meridian's arc, the same bytes twice and without Node's built-in modules, and a reader of the STEP text in `test/support/step.mjs`), the experimental IFCX print (`test/ifcx.test.mjs`: the examples' products and types as the IFC4 print counts them, each path the UUID of its GlobalId, every key with a schema and every value held to it, one tree and no cycle, the same bytes, the compiler's volumes, inheritance from types, properties, systems, ports and bodies given), and, with `EIKHUB_DIR`, EikHub's Worker. `OPENEIK_PLUMBING_TEST=<folder> npm test` also prints a folder with every pipe, duct and fixture type and holds it to Part VIII, 1.1, reading it in place.

After a change to `schemas/`, run `npm run schemas:bundle` at the root: a test fails until the bundled copy matches.

`test/conformance.test.mjs` runs the repository's [conformance suite](../../conformance/README.md) against this package, one test per case; a known failure is a todo. `test/electrical.test.mjs` tests the proposal electrical: each rule of Part XIII kept and broken, the circuits read and written, the bodies and legs, the command, and that nothing changes without it, on `test/fixtures/electrical`, an installation of two storeys as a writer of the proposal writes it (`openeik check test/fixtures/electrical --proposal electrical` gives no diagnostic). `test/conformance-proposed.test.mjs` runs the proposal's conformance cases (the proposal electrical's suite of 0.1, built by `tools/build-conformance.mjs --version 0.1 --proposal electrical` into a temporary folder; the suite of 0.2, [`conformance/`](../../conformance/README.md), folds its cases in), and checks their quotes against `spec/13-electrical.md` where the checkout has it. `test/drawings.test.mjs` tests the proposal drawings: each rule of its static checks kept and broken on the base of its conformance cases (`tools/conformance-cases-drawings.mjs`), the people read and written, issued files held up to 64 MiB and assets as bytes, and that nothing changes without it; `test/conformance-proposed.test.mjs` runs its cases too (the proposal drawings's suite of 0.1, built by `tools/build-conformance.mjs --version 0.1 --proposal drawings` into a temporary folder; the suite of 0.2, [`conformance/`](../../conformance/README.md), folds its cases in). `test/mep.test.mjs` tests heating, ventilation and plumbing (Part II, 6.9 and 13): the members and values in the schemas of 0.1, each code planted on the conformance suite's installation, a run's insulation and a deprecated `bti`, where a component stands on its pipe or its duct and its size across a duct, a duct part that names no duct, and the compiler's bodies.

`test/read-version.test.mjs` tests the reader of versions: every tree-* refusal, refused and read by a read that repairs, with what it gives back; `verify: false`, and the refusal of an object that does not hash to its name; the leaves and each record's hash against `versionTree`'s; and the writers of the two text forms against their earlier selves on random values.

## Not here

The compiler derives the class-A rules of Part IV only, and for a model of 0.2 the rules 0.2 adds, framing families and the members they make, framed walls, truss layouts and joists among them: wall joins (corners and tees, 7.1), a profiled sheet's section, roof windows and the other reference behaviour of Part IV, 2, 7.1 and 7.2 are not derived ([Compiling](#compiling)). The IFC bridge writes IFC4 only, from those bodies, the bodies an application gives and its own stand-ins, which it says are approximated: IFC 4.3 is planned for later (Part VIII, 1). It reads IFC4 and IFC2X3 once, to start a project (Part VIII, 2), and keeps no live link to the file ([Reading IFC](#reading-ifc)). It issues no drawings, and is no host and no authoring tool ([Conformance](#conformance)).
