Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FerroCHART

FerroCHART is a pure-Rust openEHR form builder and renderer. It compiles an operational template into a form definition, renders that form for a clinician, and turns the entered values back into a COMPOSITION committed to an openEHR Clinical Data Repository over the ITS-REST API. It reads compositions back into the same form, so one definition serves data entry, review, and editing.

It runs as its own server beside any openEHR CDR, and uses any FHIR terminology server to expand the value sets behind coded fields.

What a template becomes

The compiler works. It reads both ADL generations, and 121 of the 123 committed CKM templates derive a form definition, 2621 fields across 1775 groups. The server publishes that definition and a browser draws it, with a control for every field kind the derivation produces, each admitting what its template admits and refusing the rest.

That picture was taken by the end-to-end battery rather than by hand, and the renderer has the rest of the screens.

The other half of the product is the layout overlay, which carries everything no specification governs: the order the questions come in, the names and help text a person writes over the archetype’s, the values a form starts with, and the rules that decide when a question appears. The engine is built, including the replay across a template revision and the report of what matched, moved or disappeared, and the browser reads one, so a form grows as it is answered rather than asking everything at once. What is missing is the screen a person authors one on: today a layout is written by hand.

The round trip against a real CDR runs on every pull request (issue #126 is closed): the lane starts FerroEHR from the release’s own compose.yaml, commits a COMPOSITION FerroCHART built and validated, reads it back and compares. What each release adds is the build order.

The design of record is docs/architecture.md in the repository, where every decision carries a citation to a primary source or an explicit note that no specification governs it.

Why it exists

openEHR separates the clinical model from the software, which is what makes the data outlive the vendor. The cost is that a template is not a screen. Something has to turn an operational template into a form a nurse can fill in during a ward round, and turn what they typed back into a valid COMPOSITION.

The tooling that does this well is commercial. The open source options are thin enough that people running openEHR in a hospital build their own, one form at a time, or go without. That gap is the reason for this project, and it was named by the openEHR community rather than invented here.

Evaluate

What FerroCHART is for, how it is designed, what it pins, and the terms it is published under.

The problem

A template is not a screen

An openEHR operational template says what a valid COMPOSITION contains: which nodes exist, what type each carries, how many times a node may repeat, and which codes a coded field admits. It says nothing about what a person sees.

Deriving the mechanical half is deterministic. A DV_QUANTITY becomes a number with its permitted units, a DV_CODED_TEXT becomes a selection over its value set, a DV_DATE_TIME becomes a date field at the right precision, and a CLUSTER whose upper occurrence exceeds one becomes a repeatable group. No human is needed for any of it. The renderer shows what one template becomes.

The half no specification governs

Field order, grouping, labels, help text, defaults, conditional visibility and widget choice are not in the Reference Model, not in the Archetype Object Model, and not in the operational template specifications. Twelve such cases are listed in the architecture document, each one a decision a form builder must make with no specification behind it.

A person spends hours on that work. Then the template is revised.

What happens next is the whole product

Every openEHR form tool surveyed for this project loses that work, hides it, or puts it somewhere it does not belong:

  • Storing layout in the rendered artefact means nothing replays and nothing is reported when the template changes.
  • Storing layout in the template’s annotations means a form decision travels to every other consumer of that clinical model.
  • Regenerating from the definition, as HL7 FHIR Structured Data Capture does through $assemble, gives no replay and no report of what went missing.

None of them tells you what a recompile did to the layout you authored.

FerroCHART stores layout in a separate overlay keyed by node identity. A recompile from the revised template replays the overlay and reports, per entry, what matched, what disappeared, what moved, what became ambiguous and what changed type. Nothing is discarded, and nothing is silently rebound.

That report is the product.

It is built. A layout authored on all 4,447 nodes of the 121 templates this repository vendors replays with everything matched and nothing lost, and the same corpus carries the collision the key was designed for: 102 nodes across four templates where siblings share a node id and differ in nothing a key can see, one of them two identically named branches under a single event.

The design

The design of record is docs/architecture.md in the repository. It carries a decision per question with a citation to a primary source or the explicit label that no specification governs it, a pin table, a build order, and a decision register. This page is the shape of it, not a second copy.

The model comes from published crates

The openEHR Reference Model, the archetype object model and the ADL parsers come from the openehr-* crates on crates.io, which are generated from the openEHR BMM schemas. The Reference Model BMM is complete enough to generate from, and the ADL 1.4 constraint model is not, so the ADL 1.4 reader answers to the published XML schemas instead.

Both template generations, one internal model

FerroCHART reads ADL 1.4 operational templates and ADL 2 sources. The two generations express the same clinical constraint through different classes, so both readers normalize into one internal constraint model and the field derivation is written once against that. Writing it twice would double the surface where a form can admit what a template refuses.

Layout lives in an overlay, keyed by node identity

The key is a chain of steps, each carrying the RM attribute name, the node id, the archetype id where the child is an archetype root, the RM type, and the pinned name where the template states one. Where all five tie, the step carries a sibling ordinal and the entry is marked as positionally keyed.

That shape was measured rather than chosen. Walking 102 operational templates found 427 sibling groups sharing one node id under one attribute, across 14 of them. The pinned name is the only discriminator for 41.0% of those, the RM type for 29.5%, the archetype id for 17.8%, and nothing at all for 7.0%.

Real openEHR tooling agrees. Nine of the 123 CKM templates FerroCHART vendors carry a vendor layout section, and every one of its 42 entries is keyed by a path carrying archetype ids, three of them by a name predicate too. A shipped product keying layout to nodes reached the same conclusion from the other direction.

The overlay says what a person authored, and no more

Order, section, label, help text, default value, conditional visibility, the widget asked for, and geometry. Nothing else, and nothing that belongs to the template.

Geometry is a column grid. A section declares how many columns it has, an item carries a span, an optional break, and an optional width in character units. Nothing stores a row or a column index: placement comes from the order the overlay already carries, plus the span, plus the break. That makes what a reader sees and what a screen reader announces the same thing by construction, which is what WCAG 2.2 asks for and what a stored coordinate breaks.

The reason it is a grid rather than free positioning is evidence rather than taste. Of 31 form products surveyed, 25 store a device-independent layout, and of the six that store coordinates, one is a design tool, two are deprecated, two document the cost in their own documentation, and one has been redesigned twice to escape it. Clinicians meet a form on a computer on wheels and on a tablet in the same shift, so an authored form has to survive both.

A recompile reports what happened to that work

Replay the overlay against the recompiled definition and every entry comes back classified: matched, moved, disappeared, ambiguous, reordered, retyped, or a node with no layout yet. A move is a suggestion a person accepts, never applied silently. Nothing is discarded, so a revision that restores a node restores its layout.

The report reads as prose rather than a diff: how many entries were kept, how many need a decision, and for each of those what changed and what to do about it.

The compiler runs on the server

The server compiles the template and serves the form definition. The renderer reads that definition and links no engine crate, so a third party can write their own renderer against the published format.

FerroCHART owns the field-level error

A clinician gets an error on the field they got wrong, and no CDR can supply it: the openEHR ITS-REST error body is optional, conditional on a request header, and carries no path to the node that failed. So FerroCHART validates the composition against its operational template before it posts. A CDR rejecting a composition FerroCHART built and validated is a FerroCHART defect.

Build order

The tracker is the scope. Milestones are delivery promises, and a release is cut when its milestone reaches zero open issues.

Seven releases are published, v0.0.1 through v0.1.1. A template compiles to a form, a browser renders it against the layout a person authored, and the round trip against a real CDR runs on every pull request: measured against the 123 openEHR CKM templates this repository vendors, 121 read and all 121 derive, 2621 fields across 1775 groups.

This is a record of what each release added, with the current line at the end. The full order is section 14 of the architecture document, and the tracker is the live version.

ReleaseWhat it adds
v0.0.1The design of record, the workspace, the pin matrix, the gates.
v0.0.2The vendored template corpus, both template readers behind one internal constraint model, and the release supply chain.
v0.0.3The field derivation table, the form definition type, and snapshots over the whole corpus.
v0.0.4The overlay store, its geometry, replay, and the differential report.
v0.0.5The ITS-REST client, the composition builder, read-back, validation, and the terminology client.
v0.1.0The renderer: a control for every field kind, the screens it draws them on, and the published format a renderer reads.
v0.1.1A form a person can fill: the layout overlay reaching the browser, a partial date by its parts, a duration as a number and a unit, the round trip against a real CDR on every pull request, and the browser battery against the published image.

After it, v0.1.2 closes the gaps between the entered-value document and the Reference Model and turns the round trip into an instrument, and v0.2.0 is the surface a person lays a form out on. The milestones carry what is in each, and a release is cut when its milestone reaches zero open issues.

Pinned versions

Every pin in FerroCHART has one source of truth, docs/VERSIONS.md, and scripts/checks/versions.sh fails the build when a file disagrees with it.

The specifications

ComponentPin
openEHR RMRelease-1.1.0
openEHR AMRelease-2.3.0
openEHR ITS-RESTRelease-1.1.0
openEHR AQL (QUERY)Release-1.1.0
openEHR BASERelease-1.2.0
openEHR ITS-XML2.0.0
openEHR TERMRelease-3.0.0
HL7 FHIRR4 4.0.1

A component release carries documents at different maturity levels inside one number, so a citation names the component release, the document, and the section. The AM release number and the version of a document inside it are different numbers and are never conflated.

The model crates

openehr-base, openehr-rm, openehr-am, openehr-adl, openehr-its, openehr-query and openehr-term, pinned together because the line releases in lockstep and each patch is its own compatibility set.

The FHIR model is fhir-types, generated from the published HL7 FHIR packages and released on a lockstep line of its own. FerroCHART reads its R4 module, to match the FHIR pin above.

The template corpus

The openEHR CKM library, fetched by scripts/vendor/ckm-templates.sh and pinned per template by the cid that carries its asset version. CKM publishes no repository-level licence, so only the exports that state one are redistributed here.

Write your own renderer

FerroCHART publishes the form definition as a document, and everything the renderer in this repository does with it, you can do. This page is the contract: what the document contains, what a client owes it, and where the line between the two sits.

No specification governs any of it. openEHR defines the operational template the definition is compiled from and the COMPOSITION it produces, and says nothing about the shape in between. That shape is FerroCHART’s own design, so it is published, versioned, and described here rather than left to be read out of the source.

Why the format exists at all

A form builder that only its own front end can read is a front end with a storage format. The reason this one is a document is that a hospital already running a user interface should be able to keep it, and a form is not worth rewriting a clinical system over.

So the engine crates never enter a browser. scripts/checks/crate-closure.sh reads the resolved dependency graph and fails when the renderer links anything of this tree but ferrochart-form, which is the crate that holds the published types and no I/O. That check exists to keep this promise honest rather than aspirational: if the renderer needed the compiler, so would you.

The four documents

DocumentDirectionRust type
the form definitionthe server writes, a client readsferrochart_form::definition::FormDefinition
the layoutthe server writes, a client readsferrochart_form::layout::FormLayout
the entered valuesa client writes, the server readsferrochart_form::values::FormValues
the validation reportthe server writes, a client readsferrochart_form::validation::ValidationReport

Every one states format_version and a client reads that number first, refusing a document it does not know rather than guessing at a shape that may have changed underneath it.

The definition, the values and the report move together, and the current version is 2. Version 1 tagged its enums internally; version 2 tags them externally, which is the shape described below.

The layout has a version line of its own, and the current version is 1. The two documents change for different reasons: a definition changes when the derivation does, and a layout changes when what a person may author does. One number for both would refuse a layout that is in its current format.

A layout decorates a definition and never replaces it. It says the order the items come in, the label and help text over the archetype’s, the value a field starts with, and when an item is shown; it cannot make a field admit a value the template refuses. A template nobody laid out answers with a layout that decides nothing, so a client always has one to read.

Reading a definition

A definition is a tree. root is a group, a group holds items, and an item is either a nested group or a field:

{
  "format_version": 2,
  "template_id": "Ferro wire probe",
  "default_language": "en",
  "languages": ["en"],
  "root": { "key": { … }, "items": [ { "group": { … } }, { "field": { … } } ] }
}

Every enum is externally tagged: the variant is the member name and its payload is the value. So an item is {"group": {…}} or {"field": {…}}, and a field’s kind is {"quantity": {…}} rather than a kind member beside the payload. There are eighteen field kinds, one per row of the derivation table, and a client that meets one it does not know should say so on the screen rather than skip the field.

A field carries what it collects and what it admits:

{
  "rm_type": "DV_QUANTITY",
  "label": { "en": "Body temperature" },
  "occurrences": { "minimum": 0, "maximum": 1 },
  "kind": { "quantity": { "property": …, "units": [ … ] } }
}

The constraint is the point. A quantity states its permitted units, and each unit states its own magnitude range and its own decimal precision, so changing the unit changes both. A coded field states its value set. A text field states whether its list of options is the whole permitted set or a suggestion. A client that admits more than the constraint states will build a COMPOSITION the CDR rejects, and that rejection is the client’s defect.

label and help are maps from language tag to text. Where a template states nothing, the map is empty, and a client falls back to something a reader can act on rather than drawing a blank heading.

The key, and why it looks like that

Every group and field carries a key: a chain of steps from the template root, each naming the Reference Model attribute, the node id, the archetype id, the Reference Model class and the pinned name.

That is more than an identifier needs, and the reason is measured rather than theoretical. Walking 102 real operational templates showed an id-only path is not unique inside one template, so a key built from ids alone binds the wrong node. is_positional marks a key that a reordering of the template would break, which is the case a layout overlay has to report rather than silently rebind.

Writing values

FormValues is a JSON array, not an object, because its entries are keyed by a structure rather than a string. Each entry addresses one control:

[{
  "key": { "steps": [ … ], "is_positional": false },
  "group_path": [1],
  "occurrence": 0,
  "entered": { "value": { "quantity": { "magnitude": 37.2, "units": "Cel" } } }
}]

group_path is the occurrence of each repeating group above the field, outermost first, and occurrence is which repeat of the field itself. Both are needed: a repeating group produces several data nodes sharing one archetype_node_id, and without the path a value entered in the second occurrence is indistinguishable from one entered in the first. 67 of the 121 templates in the committed corpus have at least one repeating group, so this is the common case rather than an edge.

entered is {"value": …} or {"null": {"code": …, "reason": …}} and never both. openEHR RM Release-1.1.0 data_structures.html section 5.2.3 gives ELEMENT the invariant Inv_null_flavour_indicated, so an element carries exactly one of a value and a null flavour. The type is that invariant.

Placing a refusal

A ValidationReport is a list of failures. Each carries the key of the item it belongs to, so a client draws it beside that control with no path arithmetic of its own.

Two members decide where exactly:

  • at states the occurrence, where the judgement knows one. A refusal from the composition builder carries it, because the builder walks the form with that address in hand. Draw such a failure on that control alone.
  • at is absent where the judgement does not know. A refusal from the operational template carries a Reference Model path whose positional predicates do not translate to a form’s occurrence path, so it states none rather than inventing one. Draw such a failure on every repeat of its field.

A failure whose key is absent resolved to no item of the form. Show it anyway. Losing a refusal is worse than showing it in the wrong place, and a client that drops one will eventually drop the one that mattered.

What the server does that you should not reimplement

Validation. A client may check what it can as a courtesy, and the server judges the entered values against the operational template before any COMPOSITION is built, because that is where the template actually is. The routes are in Configuration.

Building the COMPOSITION. The mapping from entered values to Reference Model instances is ferrochart-compose, it is the part openEHR governs in detail, and a second implementation of it is a second place for a clinical document to go wrong.

Where the contract can change

FORMAT_VERSION covers the bytes you parse: member names, the tag and variant names of every enum, the shape of every value, and which members a document is guaranteed to carry. Two changes are deliberately not a version bump, because a client written against the old bytes still reads what it read before: a new member you may ignore, and a new variant of an enum the crate already marks #[non_exhaustive].

So write a client that ignores members it does not recognise, and that says something visible when it meets a variant it does not know. Both will happen.

Licensing

FerroCHART is source-available under the Business Source License 1.1, with no open-core tier. The compiler, the renderer, the server and the tools are in one repository under one licence, and nothing is held back to be sold back to you.

What you may do without asking

Read, build, modify and redistribute the source, without a fee. Every non-production use is covered: development, testing, evaluation and prototyping.

Production use

Production use is free for Non-Commercial Purposes, which the licence defines as personal use, academic or scientific research, teaching, and use by a non-profit organisation or public body that is not in the course of a business, does not deliver a service for payment, and is not for commercial advantage.

Any other production use needs a commercial licence from the Licensor. A hospital, clinic or care provider running FerroCHART for its patients needs one, and so does a vendor, integrator or any company running it in production. Offering FerroCHART, or a work derived from it, to third parties as a hosted, managed or embedded service needs one in every case, and so does selling, sublicensing or otherwise distributing it for a fee.

A commercial licence is arranged with Cadasto B.V., the Licensor, which handles the business side of FerroCHART: write to info@cadasto.com or use https://www.cadasto.com/contact/. Technical questions go to the maintainer named in MAINTAINERS.md.

Four years later

Each version becomes available under the Apache License 2.0 four years after its publication. That is the licence’s Change License, and it is the only place Apache 2.0 appears as a licence of this project’s own code.

Contributions

A contribution is licensed under the same licence. You keep your copyright, and you grant the Licensor the relicensing right in CONTRIBUTING.md § Licensing of contributions, recorded by a checkbox in the pull request.

Vendored material

Vendored specifications and third-party material keep their upstream terms, recorded in a PROVENANCE.md beside each vendored tree. The vendored openEHR CKM templates are CC-BY-SA-4.0 and CC-BY-SA-3.0, and nothing there is relicensed.

Operate

How to run FerroCHART, and what it needs from the world around it.

Configuration

No specification governs any of this: it is FerroCHART’s own design.

Every setting is an environment variable under one FERROCHART_ namespace, read in one place at startup. There is no configuration file and no command line flag, because a container and an orchestrator both speak environment variables and a second mechanism only creates a question about which one wins.

The variables

VariableRequiredDefaultWhat it is
FERROCHART_LISTENno127.0.0.1:8080The address to bind.
FERROCHART_CDR_URLyesThe openEHR CDR’s ITS-REST base URL.
FERROCHART_TERM_URLyesThe FHIR terminology server’s base URL.
FERROCHART_TEMPLATESnoA directory of .opt operational templates to compile at startup.
FERROCHART_OVERLAYSnoA directory of layouts, at most one per template.
FERROCHART_UInoonWhether the server serves the renderer at /ui. Reads on or off (true/false, 1/0, yes/no, in any case).
RUST_LOGnoinfoThe log filter.

The default bind is loopback. A container publishes a port by widening it to every interface in its own image, so the default never exposes a host that did not ask for it.

Both endpoints are required, and the server refuses to start without them. A form builder with no CDR to commit to and no terminology server to expand against cannot do its work. Failing at startup with the variable’s name in the message is better than failing on the first clinician’s save.

$ ferrochart
ferrochart: FERROCHART_CDR_URL is not set: the openEHR CDR's ITS-REST base URL
$ echo $?
1

The templates and their layouts

FERROCHART_TEMPLATES names a directory of .opt operational templates. The server compiles each one at startup into a form and the validator that judges what is entered against it, keyed by the identifier the template states for itself. Compiling once is what keeps the request path cheap.

FERROCHART_OVERLAYS names a directory of layouts, at most one per template. A layout is what a person decided about a form that no template states: the order of the items, the names and the help text over the archetype’s own, the values a form starts with, and the rules that decide when an item is shown. A deployment with no layouts draws every form in template order.

Both directories are optional. Unset means the server holds no template and serves no form, which is the honest reading of an operator who installed none.

A directory that IS named has to hold nothing broken. A template that will not compile fails the startup, naming the file and the cause, and so does a layout that will not read. Two templates stating one identifier fail it too, and so do two layouts over one template: a server that served fewer forms than its operator installed, or picked between two by filesystem order, would be silently wrong, and a form drawn in template order because the server dropped a layout is the failure the overlay exists to prevent.

The routes

No specification governs any of these. openEHR ITS-REST Release-1.1.0 defines a CDR’s API and says nothing about the API of a form server in front of one, so every path, body and status is FerroCHART’s own. What the bodies carry is not ours to invent: they are ferrochart-form’s published contract, and this surface transports them unchanged (Write your own renderer).

RouteWhat it answers
GET /healthThat this process is up, without authentication. It says nothing about the CDR or the terminology server.
GET /api/templatesThe template identifiers this server holds.
GET /api/templates/{template_id}/definitionThe form that template compiles to.
GET /api/templates/{template_id}/layoutThe layout a person authored over it. A template nobody laid out answers with a layout that decides nothing.
POST /api/templates/{template_id}/validationThe failures in a set of entered values, keyed onto the form. It makes no request to the CDR.
POST /api/ehrs/{ehr_id}/templates/{template_id}/compositionsBuilds, validates and commits. 201 with the version uid.
GET /api/ehrs/{ehr_id}/compositions/{uid}/values?template={template_id}A stored COMPOSITION, read back into the values of that form.

The {uid} takes either form ITS-REST accepts: one carrying :: names a version, and one without it names the versioned object and resolves to its latest.

An unknown template is 404, a body the route cannot read is 400, and values the template refuses are 422 carrying the report. A CDR that refused or never answered is 502, carrying the CDR’s own status and body rather than a flattened default. A composition the CDR reports as deleted is 410.

The renderer

The published image serves the form renderer at /ui/, and /ui redirects onto it. With the quickstart compose.yaml that address is http://127.0.0.1:8080/ui/; change the host and port with FERROCHART_BIND_HOST and FERROCHART_PORT.

The bundle is compiled into the ferrochart binary rather than copied into the image as files, so the release archive and the container image behave the same and a request path never reaches the filesystem. A path under /ui that names a file type the bundle does not hold answers 404; any other path answers the single-page document, which is how a client-side route deep-links. Content-hashed assets are served public, max-age=31536000, immutable and index.html no-cache, each with its own media type and X-Content-Type-Options: nosniff.

FERROCHART_UI=off drops the /ui routes for a deployment that wants an API-only surface. A binary built without the renderer bundle serves no /ui route whatever the variable says.

The health probe

GET /health answers 200 with the running version, and takes no authentication, because the thing that probes it is an orchestrator with no credentials.

$ curl -s http://127.0.0.1:8080/health
{"status":"ok","version":"0.1.1"}

It reports that this process is up and nothing else. It deliberately does not check the CDR or the terminology server: a probe that fails when an upstream is down takes a healthy process out of rotation for someone else’s outage.

The published container image is distroless and carries no shell, so there is no in-container health check to run. Probe the endpoint over HTTP from outside, which is what an orchestrator does anyway.

Shutdown

The process handles SIGTERM and SIGINT itself and stops serving in an orderly way. It runs as PID 1 in the image with no shell to forward a signal, so it has to.

Running it

The published compose.yaml is the shortest path. Its default starts FerroCHART alone against endpoints you supply, and its demo profile starts FerroEHR and FerroTERM alongside it so the thing runs end to end. Those are separately licensed images, and the demo profile is for evaluation.

The renderer

No specification governs any screen here: it is FerroCHART’s own design. What a field collects and what it admits come from the operational template, and the openEHR Reference Model decides what a value is. Everything about how a screen looks is ours.

The renderer is a WebAssembly bundle that reads the form definition the server publishes. It links no engine crate, so the screens below are what any client of that published format can draw.

Every picture here was taken by a test

The screenshots on this page are taken by the end-to-end battery. Nobody takes them by hand. scripts/ui-e2e.sh --docs-shots starts the server over two committed openEHR CKM templates, serves the renderer against it, drives a pinned headless Chromium through every screen, and writes one image per screen per theme into website/book/src/operate/img/renderer.

The journeys and the capture share one definition of what each screen has to have drawn before it counts as drawn, so an image is never a picture of a page that had not answered yet, and a screen the battery stopped covering cannot keep a photograph here. A run without --docs-shots writes no image at all, which is what makes the battery safe to run on every pull request.

Nothing is typed into a form during the capture. The two templates are committed CKM exports with no patient content (corpus/templates/ckm/PROVENANCE.md), and no image carries a name, an identifier, or anything a clinician entered.

Every screen is shown twice, on the light ground and on the dark one. The theme is the reader’s choice, held in their browser, and both grounds are the same semantic token names redefined, so no screen knows a colour and nothing is styled twice.

The template library

Every operational template the server compiled at startup, each one a link to the form it compiles to. A template that will not compile stops the server naming the file, so a template listed here is a template that became a form.

A form

The screen a clinician works on. What the template admits comes from the template; how the questions are ordered, named and revealed comes from the layout a person authored over it, where the server serves one. A deployment with no layout gets template order and the Reference Model tree.

A section the template says may be absent opens with none of it: its heading, its description, and the way to add one. openEHR AM Release-2.3.0 AOM1.4.html section 4.3.6 makes occurrences the count a node may appear in, so a lower bound of zero is the template saying none of that section is a complete answer. The battery opens one before it takes the picture, which is why the pictures show a form with a family member in it.

Each group is a card headed by the name a person gave it, or by the one the template gave it. A group whose occurrences allow more than one carries its bound as a badge and an add and remove pair, and it opens showing the occurrences the template requires. Each field draws the control its Reference Model type and its constraint call for: a quantity gets a magnitude and the units the template permits, a coded text gets a selection over its value set, a date gets the precision the template allows. A mandatory field is marked. Content the template left undetermined is drawn as a visible hole rather than dropped.

A field carries a value or a reason there is none, and never both: openEHR RM Release-1.1.0 data_structures.html section 5.2.3 gives ELEMENT no state that holds the two together. So a field draws its value control and a quiet “No value” beside it, and choosing a reason replaces the control rather than sitting under it. Answering “unknown” is rare and entering a value is the reason the form is open, so only one of them takes the width.

A question with a short answer takes half the row from the medium breakpoint up, so a form of dates and counts reads across as well as down instead of running several screens. Prose, an attachment, a choice and an interval keep the row, because each of them uses the width.

A date, a time or a date and time gets a native picker where the template pins one precision. Where it admits several there is no native control that collects a partial value, so the components are collected one labelled box at a time and the form assembles the ISO 8601 string, padding a number typed short. A timezone is picked from the list the browser already carries, and the form resolves it to the offset in force at the instant entered, because openEHR RM Release-1.1.0 data_types.html section 7.2.4 types DV_DATE_TIME on Iso8601_date_time and an offset is what that carries. Europe/Amsterdam is +01:00 in January and +02:00 in July, and working that out is not a clinician’s job.

The form above is laid out. Its template asks “Deceased?” as a plain question in the middle of nine others; the layout renames it “Has this family member died?”, moves the alias out of second place, and hides the date and the age at death until the answer is yes. None of that is in the template, and none of it could be: openEHR publishes no form artefact, so field order, labels, help text, defaults and conditional visibility are all authored. The layout is stored separately and keyed by node, which is what lets a template revision replay it rather than destroy it.

FERROCHART_OVERLAYS is where the server reads them from (Configuration). The surface a person authors one on is not built yet; the one this book shows was written by hand.

The design system

One screen drawing every affordance the kit defines, once. A second button style would have to appear here beside the first, which is what keeps the screens above consistent by construction rather than by review.

It is a build-time surface and a release does not carry it. Drawing every affordance means instantiating every control a second time, which cost 51506 gzipped bytes, 12.5% of what a clinician downloads. It is behind the design cargo feature, which is on by default: trunk serve brings it up at /ui/design and the browser battery photographs it, while the release bundle is built --no-default-features and answers that address the way it answers any address it does not serve.

The screens with their frame and not yet their content

Three entries on the rail lead to a heading and a line saying what the screen will do. They are photographed with the rest, because a frame nobody has seen is a frame nobody notices has gone wrong.

Where a reader lands with nothing chosen

Clicking Forms on the rail with no template named reaches the form screen with nothing to draw, so it points at the library instead.

An address no route owns says so and offers the way back. It keeps the frame: a reader who mistypes an address still has the rail, the theme control and every way out of it.

What is not built

The rail carries three entries whose screens have their frame and not yet their content. The layout overlay’s authoring surface is issue #27; a commit log of what this server posted to the CDR and a settings screen for the endpoints it uses are both still frames.

No form can commit. The server serves the routes, and the browser carries the client for them, and what is missing is the control on the form. So every screen here is a read.

Nobody can author a layout on a screen. The browser reads one and the store writes one, so the layout this book shows was written by hand. That is the authoring surface above, and it is the largest thing missing.

A section and a column grid reach no screen. The overlay model carries both, and the renderer reads the order, the labels, the help text, the defaults and the visibility rules and ignores those two. Nothing stored is discarded; a form laid out in sections draws in the Reference Model tree until they land.

Running the battery yourself

$ scripts/ui-e2e.sh

It needs cargo, cargo-nextest, trunk, curl, and a running Docker, and it owns everything it drives: it stages the templates, starts the server, serves the bundle, and runs the browser in a container. A journey that fails writes a screenshot and the whole document into target/ui-e2e-failures, and the CI job uploads that directory when a run fails.

To drive the published container image instead of a build of this tree:

$ scripts/ui-e2e.sh --image ghcr.io/ferrohealth/ferrochart:0.1.1

It pulls the image, starts it over the same two templates and the same committed layout, and drives it with the same browser. An image that answers /health and serves no /ui fails the run naming it, which is the one thing this mode exists to catch. It runs only the journeys that hold for every release, because the image is usually the last release and the rest assert what this tree does. The released workflow runs it weekly.

To drive a deployment you already have, name both ends:

$ scripts/ui-e2e.sh --base-url http://ferrochart:8180 --webdriver http://127.0.0.1:4444

Both are required together. A browser inside a container reaches a server on the host through the host gateway rather than on 127.0.0.1, and a browser that cannot reach the address is a red lane with no defect behind it.

Contribute

How this repository works, and what a change has to satisfy before it lands.

The working discipline

The specification is the oracle

The conformance authority is the openEHR Reference Model, the Archetype Object Model and ADL, the operational template specifications, openEHR ITS-REST, AQL, and the HL7 FHIR terminology service API. Never memory, and never another implementation’s behaviour. A conformance-relevant decision cites its specification and section.

The Better web template is a compatibility target. No openEHR specification defines it, every mention of it says so, and the Reference Model wins where the two disagree. The FLAT and structured formats are specified: openEHR ITS-REST Release-1.1.0 publishes simplified_formats.html in the STABLE state, and it is the authority for their media types, their field identifiers, and their Reference Model mapping.

A form never admits what the template refuses

The form definition is a projection of the operational template. A CDR rejecting a COMPOSITION that FerroCHART built and validated is always a defect here.

The gates

Every pull request runs shell, workflow and container linting, the comment-style guard, the version-matrix guard, and the Rust set: rustfmt, clippy with warnings denied, tests under --locked, doctests, rustdoc with warnings denied, an MSRV check, and cargo deny. One required check, conclusion, stands for all of them.

No gate is ever weakened to go green, and no test is skipped, weakened or edited to route around a defect it exposes.

Safety posture

unsafe is forbidden, not discouraged. Application code carries no unwrap, expect, panic! or panicking index. Recoverable failures are typed errors that carry their cause. An upstream failure is never flattened into a success or a default, because a silently wrong clinical record is worse than a loud failure.

No patient data, ever

Fixtures are synthetic content invented for the test. This project sits at the point a clinician types, so a real reproduction would be easy to create by accident.

Tracker and releases

The open issue list is the worklist. One type label and one priority label per issue, milestones are releases, and a release is cut when its milestone reaches zero open issues. Every change with a user-visible effect adds a changelog entry in the same pull request.