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 model comes from published crates
- Both template generations, one internal model
- Layout lives in an overlay, keyed by node identity
- The overlay says what a person authored, and no more
- A recompile reports what happened to that work
- The compiler runs on the server
- FerroCHART owns the field-level error
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.
| Release | What it adds |
|---|---|
v0.0.1 | The design of record, the workspace, the pin matrix, the gates. |
v0.0.2 | The vendored template corpus, both template readers behind one internal constraint model, and the release supply chain. |
v0.0.3 | The field derivation table, the form definition type, and snapshots over the whole corpus. |
v0.0.4 | The overlay store, its geometry, replay, and the differential report. |
v0.0.5 | The ITS-REST client, the composition builder, read-back, validation, and the terminology client. |
v0.1.0 | The renderer: a control for every field kind, the screens it draws them on, and the published format a renderer reads. |
v0.1.1 | A 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
| Component | Pin |
|---|---|
| openEHR RM | Release-1.1.0 |
| openEHR AM | Release-2.3.0 |
| openEHR ITS-REST | Release-1.1.0 |
| openEHR AQL (QUERY) | Release-1.1.0 |
| openEHR BASE | Release-1.2.0 |
| openEHR ITS-XML | 2.0.0 |
| openEHR TERM | Release-3.0.0 |
| HL7 FHIR | R4 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
- Why the format exists at all
- The four documents
- Reading a definition
- The key, and why it looks like that
- Writing values
- Placing a refusal
- What the server does that you should not reimplement
- Where the contract can change
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
| Document | Direction | Rust type |
|---|---|---|
| the form definition | the server writes, a client reads | ferrochart_form::definition::FormDefinition |
| the layout | the server writes, a client reads | ferrochart_form::layout::FormLayout |
| the entered values | a client writes, the server reads | ferrochart_form::values::FormValues |
| the validation report | the server writes, a client reads | ferrochart_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:
atstates 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.atis 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
- The variables
- The templates and their layouts
- The routes
- The renderer
- The health probe
- Shutdown
- Running it
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
| Variable | Required | Default | What it is |
|---|---|---|---|
FERROCHART_LISTEN | no | 127.0.0.1:8080 | The address to bind. |
FERROCHART_CDR_URL | yes | The openEHR CDR’s ITS-REST base URL. | |
FERROCHART_TERM_URL | yes | The FHIR terminology server’s base URL. | |
FERROCHART_TEMPLATES | no | A directory of .opt operational templates to compile at startup. | |
FERROCHART_OVERLAYS | no | A directory of layouts, at most one per template. | |
FERROCHART_UI | no | on | Whether the server serves the renderer at /ui. Reads on or off (true/false, 1/0, yes/no, in any case). |
RUST_LOG | no | info | The 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).
| Route | What it answers |
|---|---|
GET /health | That this process is up, without authentication. It says nothing about the CDR or the terminology server. |
GET /api/templates | The template identifiers this server holds. |
GET /api/templates/{template_id}/definition | The form that template compiles to. |
GET /api/templates/{template_id}/layout | The layout a person authored over it. A template nobody laid out answers with a layout that decides nothing. |
POST /api/templates/{template_id}/validation | The 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}/compositions | Builds, 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
- Every picture here was taken by a test
- The template library
- A form
- The design system
- The screens with their frame and not yet their content
- Where a reader lands with nothing chosen
- What is not built
- Running the battery yourself
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
- A form never admits what the template refuses
- The gates
- Safety posture
- No patient data, ever
- Tracker and releases
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.