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

Comparison with EHRbase

FerroEHR and EHRbase are two independent open-source openEHR CDRs. This page measures them side by side with the instruments FerroEHR applies to itself: the CNF 2.0 conformance runner executes the same committed catalogue against both servers, and the step-load stress instrument drives both with the same seeded clinical workload. Both directions are always published; a result that favours either server is reported exactly like one that favours the other.

EHRbase is prior art here, never an oracle. Every expected outcome comes from the openEHR specification text, so a row is red because the specification says otherwise, never because the two servers disagree.

Note

The two systems are not built or hosted alike, and the generated tables below say so. FerroEHR is built from this repository’s current sources; EHRbase runs from its official published container images. The two runs were also measured on different machines; each committed report records its own environment. Read the conformance columns as comparable (identical catalogue, identical runner, each side’s own declarations) and the performance columns as two separate measurements rather than a like-for-like hardware race.

Each side runs with its own committed party set: an ixit describing the reachable instances (EHRbase’s Basic auth carries one clinical and one admin principal and no read-only one, so its ixit declares none) and a statement (the ICS) declaring the capabilities, specification versions, and ambiguity-register options that party actually claims. A capability a party does not claim is dropped from its verdict scope and can never count against it; a case whose ground cannot exist on a party’s topology or technology profile is recorded not applicable with a machine citation, never fail. That is how the comparison stays fair without weakening a single case.

Every number and every curve on this page is generated at build time from the committed run records (results.json, verdicts.json, stress.json under docs/conformance/); nothing here is hand-typed, and a CI stale-numbers gate rejects any attempt to hand-type it. To reproduce either side yourself, see Conformance and Benchmarks.

One difference between the two projects is not something a runner can score, so this page states it rather than tabling it. FerroEHR aims to be the first openly developed, source-available openEHR CDR with a published, tracker-backed EU compliance posture, and an EHDS conformity self-assessment is on its roadmap: the compliance overview names the legal sources, the control matrix is generated from the tracker, and the shared-responsibility page says which obligations the software cannot carry. None of that is a certification claim, and none of it is a statement about EHRbase, which publishes its own documentation on its own terms.

Conformance

Systems under test

ferroehrEHRbase
Productferroehr 4.3.0ehrbase 2.34.0
Run date2026-09-152026-09-15
Party statementcommitted with the runner (declares ITS-REST pin, signing, terminology posture)committed with the runner
Stackthe project’s own compose stack, built from the current sourcesa dedicated compose stack of the official EHRbase images

Methodology

Both systems execute the same committed CNF 2.0 catalogue (1145 case-by-format executions) through the same instrument (veredictum), each on fresh volumes with its own committed party set: the ixit names the reachable instances (EHRbase declares no readonly principal), and the statement (the ICS) declares the claimed capabilities, spec versions, and ambiguity-register options — ISO/IEC 9646-style test selection excuses undeclared option branches, unclaimed capabilities, and release-dated behaviour outside the declared versions as N/A with a citation, never as silent skips. Verdicts are pure functions of (statement, results, catalogue, capability matrix).

The declared-version delta matters and is stated, not hidden: ferroehr declares ITS-REST 1.1.0 while EHRbase declares ITS-REST 1.0.3 — the catalogue realizes 1.1.0, so every Release-1.1.0-dated behaviour (the Demographic API, ITEM_TAGs, Simplified Formats on the wire, the admin EHR delete, the weak-ETag/Location header forms, …) is cited N/A for the 1.0.3 declaration rather than driven against a release EHRbase never claimed. The verdict-bearing comparison below is therefore each party’s in-scope subset, never the raw record.

Profile verdicts

ProfileferroehrEHRbase
COREpassfail
STANDARDpassfail
OPTIONSpassnot claimed
SEC-BASICpassnot claimed

In-scope outcomes

Runs compared: ferroehr (run of 2026-09-15) vs EHRbase 2.34.0 (run of 2026-09-15) — the SAME catalogue through the same runner, each with its own committed party statement. Per the presentation rule, the headline is each party’s VERDICT SCOPE (the cases its own declarations select), never the raw record: a raw count would book release-dated and unclaimed surfaces against a party that never claimed them.

verdict scope (selected)drivenin-scope passedin-scope failedin-scope inconclusive
ferroehr11451104110400
EHRbase692571148148275

An inconclusive row’s wire answered outside the operation’s bound outcome map, or its required ground could not be established (e.g. a refused provisioning exchange) — never counted as a failure of the behaviour under test. Every not-run row in the full committed record (docs/conformance/<sut>/results.json) carries a machine-readable citation: an undeclared option branch, an unclaimed capability, a release-dated behaviour outside the declared spec versions, or a ground the party’s topology cannot establish.

Capability-by-capability

Evidence tokens from each party’s computed verdicts: passed (every gating case green), failed (at least one gating case red), inconclusive (a gating case neither passed nor failed cleanly), not_evidenced (claimed, but no gating case produced a verdict — there is no excused state: a required capability without passing evidence fails its tier, whichever party claims it), or not_claimed (absent from that party’s ICS, so no case naming it was ever selected — it can never count for or against that party). The whole capability matrix is listed for both columns, because the matrix is the profiles book as data, not a claim list.

CapabilityferroehrEHRbase
ActivityReportpassednot_claimed
Adl14ArchetypeProvisioningpassedfailed
Adl14OptProvisioningpassedfailed
Adl2ArchetypeProvisioningpassednot_claimed
Adl2OptProvisioningpassednot_claimed
AdminApipassednot_evidenced
AnonymousEhrspassednot_claimed
AqlAdvancedpassedinconclusive
AqlBasicpassedfailed
AqlTerminologypassednot_claimed
ArchetypeValidationpassedfailed
AuditAccountabilitypassednot_claimed
AuthenticatedAccesspassedpassed
AuthorizationSeparationpassednot_evidenced
BulkEhrLoadpassednot_claimed
ChangeSetspassedfailed
CompositionOpspassedinconclusive
DefinitionApipassedfailed
DemographicApipassednot_claimed
DemographicArchetypeValidationpassednot_claimed
DemographicArchivepassednot_claimed
DirectoryOpspassedfailed
EhrApipassedfailed
EhrArchivepassednot_claimed
EhrDemographicSeparationpassedpassed
EhrDumpLoadpassednot_claimed
EhrExtractpassednot_claimed
EhrOperationspassedfailed
EhrStatuspassedfailed
ItemTagspassednot_claimed
MessageApipassednot_claimed
PartyOperationspassednot_claimed
PartyRelationshipOperationspassednot_claimed
PhysicalDeletionpassednot_evidenced
QueryApipassedfailed
QueryProvisioningpassedfailed
Signingpassednot_claimed
SimplifiedFormatspassednot_claimed
SmartAppLaunchpassednot_claimed
SystemApipassednot_claimed
Tdspassednot_claimed
TemplateExamplespassednot_evidenced
Versioningpassedfailed

Failures — both directions

ferroehr failures (with the EHRbase outcome on the identical case)

CaseFormatFailureEHRbase outcome
none — zero failing cases

EHRbase failures by schedule chapter

Chapterfailed cases
CONT67
I_EHR_STATUS27
I_EHR_CONTRIBUTION13
I_EHR_DIRECTORY13
I_DEFINITION_QUERY10
I_DEFINITION_ADL148
I_EHR_SERVICE4
I_QUERY_SERVICE4
I_EHR_COMPOSITION1
I_ITS_REST_REVISION_HISTORY1
Every EHRbase-failed case, with the ferroehr outcome on the identical case
CaseFormatEHRbase failureferroehr outcome
CONT-COMP-content_card_1plus-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_1plus-context_mandexpected created, observed validation_failedpassed
CONT-COMP-content_card_3plus-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_3plus-context_mandexpected created, observed validation_failedpassed
CONT-COMP-content_card_3to5-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_3to5-context_mandexpected created, observed validation_failedpassed
CONT-COMP-content_card_any-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_any-context_mandexpected created, observed validation_failedpassed
CONT-COMP-content_card_mand-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_mand-context_mandexpected created, observed validation_failedpassed
CONT-COMP-content_card_opt-context_anyexpected created, observed validation_failedpassed
CONT-COMP-content_card_opt-context_mandexpected created, observed validation_failedpassed
CONT-COMPOSITION-content_cardinality_count6expected created, observed validation_failedpassed
CONT-COMPOSITION-context_existenceexpected created, observed validation_failedpassed
CONT-DV_CODED_TEXT-validate_openexpected bad_request, observed validation_failedpassed
CONT-DV_DATE-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_DATE-validate_rangeexpected created, observed validation_failedpassed
CONT-DV_DATE_TIME-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_DATE_TIME-validate_rangeexpected created, observed validation_failedpassed
CONT-DV_DURATION-validate_fieldsexpected created, observed validation_failedpassed
CONT-DV_DURATION-validate_fields_rangeexpected created, observed validation_failedpassed
CONT-DV_DURATION-validate_rangeexpected created, observed validation_failedpassed
CONT-DV_IDENTIFIER-validate_all_listexpected created, observed validation_failedpassed
CONT-DV_IDENTIFIER-validate_all_patternexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DATE-validate_lower_upper_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DATE-validate_lower_upper_rangeexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DATE_TIME-validate_lower_upper_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DATE_TIME-validate_lower_upper_rangeexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DURATION-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_DURATION-validate_rangeexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_ORDINAL-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_PROPORTION-validate_ratio_rangeexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_SCALE-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_TIME-validate_lower_upper_constraintexpected created, observed validation_failedpassed
CONT-DV_INTERVAL_DV_TIME-validate_lower_upper_rangeexpected created, observed validation_failedpassed
CONT-DV_MULTIMEDIA-validate_media_typeexpected created, observed validation_failedpassed
CONT-DV_PARSABLE-validate_value_formalismexpected created, observed validation_failedpassed
CONT-DV_TEXT-validate_openexpected bad_request, observed validation_failedpassed
CONT-DV_TIME-validate_constraintexpected created, observed validation_failedpassed
CONT-DV_TIME-validate_rangeexpected created, observed validation_failedpassed
CONT-EVENT-state_ex_mandexpected bad_request, observed validation_failedpassed
CONT-EVENT-state_ex_optexpected bad_request, observed validation_failedpassed
CONT-EVENT-type_anyexpected created, observed validation_failedpassed
CONT-EVENT-type_interval_eventexpected created, observed validation_failedpassed
CONT-EVENT-type_point_eventexpected created, observed validation_failedpassed
CONT-HIST-events_card_1plus-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_1plus-summary_ex_optexpected created, observed validation_failedpassed
CONT-HIST-events_card_3plus-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_3plus-summary_ex_optexpected created, observed validation_failedpassed
CONT-HIST-events_card_3to5-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_3to5-summary_ex_optexpected created, observed validation_failedpassed
CONT-HIST-events_card_any-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_any-summary_ex_optexpected created, observed validation_failedpassed
CONT-HIST-events_card_mand-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_mand-summary_ex_optexpected created, observed validation_failedpassed
CONT-HIST-events_card_opt-summary_ex_mandexpected created, observed validation_failedpassed
CONT-HIST-events_card_opt-summary_ex_optexpected created, observed validation_failedpassed
CONT-HISTORY-events_cardinality_count6expected created, observed validation_failedpassed
CONT-ITEM_STR-type_anyexpected created, observed validation_failedpassed
CONT-ITEM_STR-type_item_listexpected created, observed validation_failedpassed
CONT-ITEM_STR-type_item_singleexpected created, observed validation_failedpassed
CONT-ITEM_STR-type_item_tableexpected created, observed validation_failedpassed
CONT-ITEM_STR-type_item_treeexpected created, observed validation_failedpassed
CONT-OBS-state_ex_mand-protocol_ex_mandexpected bad_request, observed validation_failedpassed
CONT-OBS-state_ex_mand-protocol_ex_optexpected bad_request, observed validation_failedpassed
CONT-OBS-state_ex_opt-protocol_ex_mandexpected bad_request, observed validation_failedpassed
CONT-OBS-state_ex_opt-protocol_ex_optexpected bad_request, observed validation_failedpassed
I_DEFINITION_ADL14.delete_archetype-clinical_forbiddenexpected forbidden, observed not_foundpassed
I_DEFINITION_ADL14.upload_opt-invalid_optexpected validation_failed, observed not_acceptablepassed
I_DEFINITION_ADL14.upload_opt-largest_publishedexpected created, observed not_acceptablepassed
I_DEFINITION_ADL14.upload_opt-valid_optexpected created, observed not_acceptablepassed
I_DEFINITION_ADL14.upload_opt-valid_opt_twice_conflictexpected created, observed not_acceptablepassed
I_DEFINITION_ADL14.upload_opt-valid_opt_twice_no_conflictexpected created, observed not_acceptablepassed
I_DEFINITION_ADL14.validate_opt-invalid_optexpected validation_failed, observed not_acceptablepassed
I_DEFINITION_ADL14.validate_opt-valid_optexpected created, observed not_acceptablepassed
I_DEFINITION_QUERY.list_queries-prefix_all_versions[0]/name: path resolves to nothingpassed
I_DEFINITION_QUERY.list_queries-version_get_xml_not_acceptableexpected not_acceptable, observed okpassed
I_DEFINITION_QUERY.list_queries-xml_not_acceptableexpected not_acceptable, observed okpassed
I_DEFINITION_QUERY.store_query-default_slot_with_higher_versionheader Location: value “http://localhost:8091/ehrbase/rest/openehr/v1/definition/query/orgpassed
I_DEFINITION_QUERY.store_query-dotted_nameexpected stored, observed bad_requestpassed
I_DEFINITION_QUERY.store_query-unqualified_nameexpected stored, observed bad_requestpassed
I_DEFINITION_QUERY.store_query-update_in_placeheader Location: value “http://localhost:8091/ehrbase/rest/openehr/v1/definition/query/orgpassed
I_DEFINITION_QUERY.store_query-version_duplicate_case_variant_nameexpected conflict, observed storedpassed
I_DEFINITION_QUERY.store_query-version_prefix_rejectedexpected bad_request, observed storedpassed
I_DEFINITION_QUERY.store_query-version_prerelease_rejectedexpected bad_request, observed storedpassed
I_EHR_COMPOSITION.get_versioned_composition-malformed_uidexpected bad_request, observed not_foundpassed
I_EHR_CONTRIBUTION.commit_contribution-delete_directoryexpected created, observed not_foundpassed
I_EHR_CONTRIBUTION.commit_contribution-deleted_member_with_dataexpected validation_failed, observed createdpassed
I_EHR_CONTRIBUTION.commit_contribution-ehr_status_incomplete_lifecycleexpected validation_failed, observed createdpassed
I_EHR_CONTRIBUTION.commit_contribution-ehr_status_invalid_change_typeexpected conflict, observed validation_failedpassed
I_EHR_CONTRIBUTION.commit_contribution-ehr_status_invalid_change_type_deletedexpected conflict, observed not_foundpassed
I_EHR_CONTRIBUTION.commit_contribution-ehr_status_valid_combinationsheader Last-Modified: expected present, got nonepassed
I_EHR_CONTRIBUTION.commit_contribution-fail_modify_non_existing_directoryexpected validation_failed, observed precondition_failedpassed
I_EHR_CONTRIBUTION.commit_contribution-full_ehr_statusheader Last-Modified: expected present, got nonepassed
I_EHR_CONTRIBUTION.commit_contribution-minimal_ehr_statusheader Last-Modified: expected present, got nonepassed
I_EHR_CONTRIBUTION.commit_contribution-non_exiting_optexpected template_not_found, observed validation_failedpassed
I_EHR_CONTRIBUTION.commit_contribution-update_existing_directoryheader Last-Modified: expected present, got nonepassed
I_EHR_CONTRIBUTION.commit_contribution-valid_directoryheader Last-Modified: expected present, got nonepassed
I_EHR_CONTRIBUTION.get_contribution-version_ref_shapeheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.create_directory-ehr_not_modifiableexpected updated, observed precondition_missingpassed
I_EHR_DIRECTORY.create_directory-root_archetype_id_mismatchexpected validation_failed, observed createdpassed
I_EHR_DIRECTORY.create_directory-versioned_id_itemsexpected created, observed bad_requestpassed
I_EHR_DIRECTORY.create_directory-wide_id_itemsexpected created, observed bad_requestpassed
I_EHR_DIRECTORY.delete_directory-ehr_with_directoryheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.delete_directory-empty_ehrexpected not_found, observed precondition_failedpassed
I_EHR_DIRECTORY.delete_directory-etag_names_new_versionheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.get_directory-deleted_headheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.get_directory_at_time-deleted_at_timeheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.get_directory_at_version-deleted_versionheader Last-Modified: expected present, got nonepassed
I_EHR_DIRECTORY.update_directory-empty_ehrexpected not_found, observed precondition_failedpassed
I_EHR_DIRECTORY.update_directory-root_archetype_id_mismatchexpected validation_failed, observed updatedpassed
I_EHR_DIRECTORY.update_directory-stale_if_matchheader ETag: expected the latest version uid, got nonepassed
I_EHR_SERVICE.create_ehr-committal_headerscommit_audit/description/value: path resolves to nothingpassed
I_EHR_SERVICE.create_ehr-invalid_statusexpected validation_failed, observed createdpassed
I_EHR_SERVICE.create_ehr-wrong_methodheader Allow: expected a value matching “.*(GET.*POST|POST.GET).”, got nonepassed
I_EHR_SERVICE.get_ehr-malformed_ehr_idexpected bad_request, observed not_foundpassed
I_EHR_STATUS.clear_ehr_modifiable-bad_ehrexpected not_found, observed precondition_missingpassed
I_EHR_STATUS.clear_ehr_modifiable-existing_ehrexpected updated, observed precondition_missingpassed
I_EHR_STATUS.clear_ehr_modifiable-invalid_bodyexpected validation_failed, observed bad_requestpassed
I_EHR_STATUS.clear_ehr_modifiable-stale_if_matchexpected updated, observed precondition_missingpassed
I_EHR_STATUS.clear_ehr_queryable-bad_ehrexpected not_found, observed precondition_missingpassed
I_EHR_STATUS.clear_ehr_queryable-existing_ehrexpected updated, observed precondition_missingpassed
I_EHR_STATUS.clear_ehr_queryable-invalid_bodyexpected validation_failed, observed bad_requestpassed
I_EHR_STATUS.clear_ehr_queryable-stale_if_matchexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_ehr_status-at_time_futureexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_ehr_status-at_time_omittedexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_ehr_status_at_version-addressed_versionexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_versioned_ehr_status-at_time_futureexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_versioned_ehr_status-at_time_omittedexpected updated, observed precondition_missingpassed
I_EHR_STATUS.get_versioned_ehr_status-contained_uid_formheader Last-Modified: expected present, got nonepassed
I_EHR_STATUS.get_versioned_ehr_status-container_shapeowner_id/type: “ehr” != expected “EHR”passed
I_EHR_STATUS.get_versioned_ehr_status-xmlcanonical-xmlheader Last-Modified: expected present, got nonepassed
I_EHR_STATUS.set_ehr_modifiable-bad_ehrexpected not_found, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_modifiable-existing_ehrexpected updated, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_modifiable-invalid_bodyexpected validation_failed, observed bad_requestpassed
I_EHR_STATUS.set_ehr_modifiable-missing_if_matchexpected updated, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_modifiable-stale_if_matchexpected updated, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_queryable-bad_ehrexpected not_found, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_queryable-existing_ehrexpected updated, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_queryable-invalid_bodyexpected validation_failed, observed bad_requestpassed
I_EHR_STATUS.set_ehr_queryable-missing_if_matchexpected updated, observed precondition_missingpassed
I_EHR_STATUS.set_ehr_queryable-rm_version_emptyexpected validation_failed, observed bad_requestpassed
I_EHR_STATUS.set_ehr_queryable-stale_if_matchexpected updated, observed precondition_missingpassed
I_ITS_REST_REVISION_HISTORY.versioned_ehr_status_revision_history-two_versionscanonical-jsonexpected updated, observed precondition_missingpassed
I_QUERY_SERVICE.execute_ad_hoc_query-bare_ehr_scope_headerrow count 100 != expected 1passed
I_QUERY_SERVICE.execute_ad_hoc_query-bare_ehr_scope_paramrow count 100 != expected 1passed
I_QUERY_SERVICE.execute_ad_hoc_query-unknown_ehr_scopeexpected not_found, observed okpassed
I_QUERY_SERVICE.execute_stored_query-fetch_with_topexpected stored, observed bad_requestpassed

Both servers’ capability conformance, from each party’s committed verdicts (generated, diff-guarded; see Conformance for how to read the grid):

And the per-chapter outcomes side by side. Both charts render the same chapter-and-band taxonomy, so they read band-for-band: a band EHRbase did not exercise shows as an explicit no cases row in the same position. Compare the printed counts, not the bar lengths: each chart scales its bars to its own widest band, and the legend states that scale.

How to read the EHRbase column

Three mechanisms decide what the EHRbase column can say, and all three are visible in the committed record:

  • Selection. A case is in EHRbase’s verdict scope only if its capabilities intersect what EHRbase claims and its behaviour is dated at or below the specification versions EHRbase declares. EHRbase declares ITS-REST 1.0.3 while the catalogue realizes 1.1.0, so every Release-1.1.0-dated behaviour is cited out of scope rather than driven. Unclaimed surfaces drop out the same way, and they drop out before the request: a case whose capabilities a party’s statement does not claim is recorded not applicable with its citation instead of being driven, so an unclaimed surface cannot produce a red row at all. Anything a record still carries from before that rule reached the driver is excluded from the in-scope tally above and is not listed as a divergence below. The headline is the verdict scope for exactly that reason.
  • Inconclusive, not failed. A row is inconclusive when EHRbase answered with a status the operation’s specification-cited outcome map does not contain, or when the exchange that would have established the case’s ground was refused. The runner never guesses a verdict, and an inconclusive row is never counted as a failure of the behaviour under test.
  • Report-only grounds. Where a case rests on an openEHR silence our ambiguity register marks report-only (an upstream problem report still open) the row is recorded but does not gate either party’s verdicts.

Where the red rows do concentrate is the content chapter: no content case passed (each one either failed or never established its ground) and the direction is the opposite of a permissive server. The catalogue’s content cases are accept/reject decision tables committed against a template the case itself provisions, and in the ones that ran, EHRbase’s rejected rows pass while its accepted rows fail: it refuses documents its own template admits. Some rows require a plain bad request rather than a semantic refusal (the document is malformed, not merely invalid), and there EHRbase refuses too, in the wrong class.

The principal EHRbase divergences, stated plainly

Each item below is a red row in the committed EHRbase record, restated from what that record holds: the outcome the specification-cited expectation required versus the outcome observed. Except where marked, every one was selected for EHRbase’s own declared ITS-REST 1.0.3; the Release-1.1.0-dated behaviours never reach this list. The record carries the case id, the failing step, the rows driven and the reason string. It is not a wire transcript, so nothing is quoted here beyond what it actually captured.

  • Every EHR_STATUS write is refused. Setting or clearing is_queryable/is_modifiable is realized as a read-modify-write: read the status, flip the flag, PUT it back with the version identifier from the server’s own ETag in a quoted If-Match, the exact form the released overview’s own example shows. Every one of those rows, including the plain happy path, answers 400 instead of updating: the status the same released section reserves for a client that did not provide If-Match at all. The rest of that path is therefore unreadable: a row meant to distinguish a semantic refusal from a malformed request gets that same 400, so the record cannot say what EHRbase’s validation would have done.
  • No Last-Modified anywhere. The released overview says both ETag and Last-Modified SHOULD be included for versioned resources; EHRbase sends no Last-Modified on any of them: contribution commits, contribution reads, directory reads, updates and deletes, and the versioned EHR_STATUS reads in JSON and XML alike. It is the single widest header difference in the record. Relatedly, a stale If-Match on a directory update is refused without the ETag naming the latest version that the refusal is supposed to carry.
  • Some model-invalid content is accepted. POST /ehr with an EHR_STATUS that is an archetype root carrying no archetype_details is created rather than refused, while the four sibling rows in the same case (missing mandatory members, an undecodable polymorphic slot) are refused correctly, so this is a validation gap, not a missing validator. A directory whose root archetype identifier does not match the one required is likewise accepted, on both the create and the update path, and a CONTRIBUTION member marked deleted that still carries data commits instead of being refused.
  • Contribution refusals land in the wrong family. A CONTRIBUTION whose EHR_STATUS member carries an invalid change type is refused as a semantic validation error where the catalogue requires a conflict; the variant that deletes the mandatory EHR_STATUS answers not-found instead. A CONTRIBUTION naming a template that does not exist answers a generic validation error rather than the template-not-found branch, and a modification against a directory that does not exist answers a failed precondition.
  • A missing directory is dressed as a failed precondition. Both the update and the delete of a directory on an EHR that has none answer 412 Precondition Failed where the released text requires not-found. HTTP requires the opposite order, since a failure detectable before the precondition is evaluated takes precedence over evaluating it. On the same operation a genuinely stale If-Match answers 409, a status the operation’s outcome map does not contain at all, so that row is recorded inconclusive rather than red.
  • A malformed identifier in the path answers not-found, not bad request. Given a path segment that is not a UUID at all, both the EHR read and the versioned-COMPOSITION read report a miss instead of a client error. The treatment is not uniform: the malformed version-identifier rows pass.
  • Stored-query names the released grammar admits are refused. The released qualified-name grammar makes the namespace optional (it lists a plain my_compositions among its valid examples) and puts the dot inside the query-name character set, with :: as the only separator. Both a plain unqualified name and a namespace-less dotted name are refused as bad requests. A third store (a fully qualified name whose AQL carries a TOP) is refused as well, so the paging conflict that case exists to test never got driven.
  • The stored-query listing does not honour the prefix. The released list operation’s own worked example lists “all versions of all queries with names starting with org.openehr”. After storing two names under one prefix, the case’s prefix listing comes back with no first row to read a name from at all, so a client cannot discover what a namespace holds.
  • Version-grammar refusals are missing at the store. A version that is only a prefix and a pre-release version are both stored where the catalogue requires a bad request, and a second store under a case-variant of an existing name is accepted where a conflict is required.
  • An XML Accept on a JSON-only operation is answered, not refused. The released stored-query listing declares Accept: application/json and nothing else, and the overview requires 406 Not Acceptable when an XML request cannot be fulfilled. Both the listing and its version-get sibling answer success instead.
  • A query scoped to one EHR is executed over all of them. A client restricts an ad-hoc query to a single record with the ehr_id query parameter or the openehr-ehr-id request header. The service model defines that input as the specific set of EHRs on which to execute the query, and the released REST query chapter says the same thing from the other side: a population query is one that does not use the parameter to constrain the scope. EHRbase discards both carriers. In the record, a bare projection over EHRs scoped to one freshly created EHR comes back at the request’s own row limit instead of the single scoped row, and a scope naming an EHR that does not exist answers an ordinary result set rather than reporting the unknown EHR. Reproduced live against a composed EHRbase, its own response metadata discloses the query it actually executed, with a row limit appended and no EHR predicate added; the header behaves exactly like the parameter. Naming the EHR inside the AQL works, so what is dropped is specifically the request-level scope. Stated at its true strength: the released REST text puts support for these parameters at SHOULD and the semantics are the service model’s, so this is a divergence from service-model semantics on a released carrier, not the breach of a REST-level MUST.
  • A 405 carries no Allow header, which RFC 9110 makes mandatory on that status; and the versioned-EHR_STATUS container names its owner with a lower-case ehr type where the Reference Model’s object reference spells it EHR.
  • (1.1.0-grounded) The template upload refuses a JSON Accept. Asked for application/json it answers 406 (the refusal body the record captured reads "No acceptable representation") and serves XML only, while the released parameter enumeration for that header lists JSON first. This single refusal is what makes most content-chapter rows inconclusive: the runner’s provisioning uploads ask for JSON, EHRbase refuses, and the case’s ground never exists.

Two things the record deliberately does not support, and therefore are not claimed here: it captures no response bodies beyond the few a case records, so no error text is quoted above that the record does not contain; and it says nothing about EHRbase’s canonical-XML root namespace: the case that would have established it never got past provisioning, and the negotiated-namespace siblings are out of scope for EHRbase’s declared options.

Where the difference is a reading of a silence

Some red rows are not EHRbase’s fault, and are called out rather than counted.

  • Which version a version-less stored-query store lands on. A store with no version in the URL has to land at some version, and no released sentence says which. Our suite pins the lowest slot because a suite must pin something; EHRbase continues the existing series instead, so a store beside an already-stored higher version takes the next one up. Both are defensible readings, so those two rows record a difference of house convention. The open question is with openEHR, not with either implementation.

One half of the EHR-scope row above is a silence too, and a much narrower one than this page previously claimed: the service model declares an error for a query scoped to an EHR that does not exist, and the released REST text binds that error to no status, so which refusal a conformant server owes is genuinely open, and our suite resolves it to not-found. What the scope does is not open, which is why that row is published as a divergence rather than as a reading.

Every silence is recorded in the runner’s typed ambiguity register and reported upstream: a specification silence is never resolved privately, and never resolved by looking at what a server happens to do.

Performance

Both systems run the same committed step-load stress instrument (veredictum stress) on their own freshly seeded cnf.scale.10k corpus: the geometric ladder climbs until the system leaves the envelope (p99 over the budget or errors past tolerance), then bisects to the maximum sustainable throughput. Every number derives from the two committed stress.json reports; each load step embeds re-checkable histograms and, where sampled, per-container resource telemetry.

max sustainable throughputworst p99 at the kneeDB peak CPU at the kneeSUT peak RSS at the knee
ferroehr512 req/s134 ms101 %247 MB
EHRbase0 req/s— ms— %— MB

A stress report is exploration evidence: it earns no conformance class (classes are earned exclusively by the hour-long measured class runs) and carries no class vocabulary — the chart shows where each system breaks.

Reading the stress table honestly

The step-load ladder declares a rate sustained only if the whole envelope holds at that rate: the latency budget and the error budget. EHRbase’s ladder found no sustained rung, and its own report records two simultaneous breaches on every rung it tried, down to the lowest one:

  • The error tolerance. Composition commits and updates, contribution commits, EHR_STATUS updates, directory creates and the item-tag operations error deterministically; these are the same wire refusals the conformance section above lists, met again under load.
  • The latency budget, on the ward-round query, by more than an order of magnitude.

Because both breaches are already present at the ladder’s lowest rung, neither is a throughput ceiling: EHRbase’s entry in the generated table says the envelope never held, not how fast the server can go, and it must not be read as a speed comparison. The per-operation latencies its report carries are honest observations from those same rungs.

Method, in one paragraph

The conformance instrument derives every expected outcome from the openEHR specifications (never from either server’s observed behaviour) and runs against real composed deployments of both systems (scripts/conformance.sh, CONF_SUT=ehrbase for the EHRbase side). The stress instrument drives the same hospital-simulation workload (admissions, observations, medication rounds, lab contributions, chart reviews, corrections, discharges) built from official CKM templates with seeded determinism, so both servers receive byte-identical requests from the same corpus; latencies are coordinated-omission-corrected from each request’s planned arrival instant, and the ladder bisects to the last rate held inside the envelope. Each run records its own environment, and the two runs’ environments differ. The full method chapters: Conformance · Benchmarks.