Repository navigation
0.0.6
The v0.0.4, v0.0.5 and v0.0.6 milestones in one release (no v0.0.4 or v0.0.5
tag was cut). The federated query now shapes its answer across nodes the way
a single CDR would: ORDER BY with LIMIT re-applied at the Tier, bounded
OFFSET pages, DISTINCT, decomposable aggregates and opt-in
version-identity dedup, each refused with a reason where it cannot be exact.
Every failure carries its §11.2 status and a stable error code. A request
under {base}/v1/ehr/{ehr_id} is routed to the one node that holds that EHR,
byte-identical, and every answer teaches the gateway which system created
each version. A client names nodes through the FROM ENDPOINT /
ORGANISATION directive or the targeting headers and selects their ENDPOINT
attributes, the registry also loads as FHIR Endpoint and Organization
resources, stored queries are held at the gateway as immutable versions, and
OPTIONS {base}/ declares all of it. The identifier-hygiene property is now
held by the track 10 leakage suite against both nodes: no client-chosen
request id, targeting header or path segment carries a patient identifier to
a node or into the log.
Added
-
The follow-up routing table learns from every answer (#64; §12.2, N21,
CP-13). Each version uid in a federated query's rows, and each one a
routed read names or answers with in itsETag, teaches a route for a
creating_system_idthe registry document does not map; one seen at two
nodes raises theLearnedCreatingSystemConflictintegrity incident and is
routed on by neither. CP-13 is now scored. A follow-up read of a version
under{base}/v1/ehr/{ehr_id}/…routes by the pathehr_idin the order
of N41, never by the version'screating_system_id(§12a.1, §12.5.1, N22,
N42a), and reaches the node byte-identical. -
The federated stored-query registry (#77; §12.7, N44, N33, CP-40,
CP-28). With[stored_queries] pathset, the gateway holds stored queries
itself in an embeddedredbfile that survives a restart, and
OPTIONS {base}/declaresdefinition.stored_query_registry: true.
PUT {base}/v1/definition/query/{name}/{version}stores the AQL on
ITS-REST's semver segment: the name is[{namespace}::]{query-name}, the
versionmajor.minor.patch, and the text is analysed as an inline query
would be, with each$parameterstanding in for a bound value, and held
as its canonical print. A definition that names the patient by a literal
is refused400 subject-literal, so no patient identifier is held at
rest; a secondPUTof a held name and version is refused
409 stored-query-heldand the held text stands, across a restart too.
GETon the same path reads the ITS-RESTStoredQueryback, and
GET {base}/v1/definition/query/{pattern}lists the versions of every
name the pattern starts.POST {base}/v1/query/{name}[/{version}]runs
the stored query over every member exactly as if its text were sent
inline, with the client'soffset,fetchandquery_parameters, the
targeting, completeness and dedup headers and the budget; without a
version the highest runs, and a{major}or{major}.{minor}prefix runs
the highest it matches. The answer carries ITS-REST'snamenaming the
gateway's definition. New codes:query-name-invalid,
query-version-invalid,query-version-required,query-type-unsupported,
subject-literal(each400),stored-query-held(409) and
stored-query-unknown(404). Without[stored_queries]the definition
routes still answer501.openehr-federationis 0.0.30 and adds
aql::definition::Definition, the admission analysis of a stored
definition. Definition fan-out to the nodes is #78. -
Routing a path
ehr_idin the order of §12.5.1 (#62; §12.5, N41,
CP-33). A request to{base}/v1/ehr/{ehr_id}or below it goes to the node
the targeting headers name, then the node a resolution binding of the
client session names, then the node the newehr_idindex names, and for
a read only, the one member an ask-all probe finds. The probe sends
GET {base}/v1/ehr/{ehr_id}to every member at once within the per-node
timeout and the overall budget (§11.5); a read of the EHR itself is
answered from the owner's probe answer. A read no member holds is
404 no-destination, one two members hold is409 ehr-id-collision
listing the claimants, and one a member did not answer for is504
(node-timeout,node-unreachable) or424(node-refused, and the new
codenode-error), naming the member: a member that gave no answer may
hold theehr_id, so the owner is unknown. A read that names no node no
longer answers501. A write none of the first three steps routes stays
400 target-required, and nothing is probed. A step that names two
members names none, and no later step picks one of them. The path
ehr_idmust be an openEHRHIER_OBJECT_ID, and any other value is400
with the new code
ehr-id-invalidbefore any routing. The index learns from resolutions and
from members' successful answers, holdsehr_ids and member ids only, in
memory, and forgets the least recently usedehr_idpast
federation.ehr_index_capacity(default 100000; 0 is refused). Session
bindings answer once client authentication lands (#80); the integrity
incident of a collision is #63. -
OPTIONS {base}/, the gateway's self-description (#73; §7a.2, N30,
CP-23). The body validates against the vendoredoptions-root.schema.json
and is built from the running configuration: the federation id, the
major.minorof the pinned specification,aql.fan_outfrom the node
selection, the dedup default, modes and request header, the per-node and
overall budgets, all-or-nothing with the best-effort opt-in when it is
offered, theOFFSETstrategy (boundedwith itsmax_window, or
reject, never a cursor), the decomposable aggregates (an empty list when
none), nothing offered in the definition area,localization.on_failure: "closed", the ITS-REST areas, and every registry endpoint with its
organisation, membership status, node,system_id, and product and
version where the registry holds them. It declares no targeting mechanism,
no resolution carrier, no asynchronous queries and no JWKS location, and it
needs no patient identifier.OPTIONSon a path under{base}/v1/
answers204with the methods served there inAllow, without asking a
node, and501where the gateway serves nothing. -
ENDPOINT attributes in rows (#72; §9.2, §9.3, §9.4, N12, N17, N18,
CP-35, CP-37). A directed query that selectsp/idorp/endpoint_id,
p/organisationorp/organization_id,p/system_idorp/urlthrough
theFROM ENDPOINTvariable gets each value in every row, as a string, from
the registry entry of the endpoint the row came from: the values
meta.federation.endpoints[]reports for it. No node is asked for an
attribute, and a query that selects none keeps the row shape of a single
CDR. The §9.4 example answers the §9.4 columns and rows, withcolumns[]
paths in the ITS-REST form (/id,/system_id,/uid/value). An alias on
an attribute keeps it apart from an EHR-derived column of the same name;
giving both the same name is refused400with the new refusal
endpoint-name-collision, and a path through the variable that is no §9.3
attribute withendpoint-attribute-unknown. UnderDISTINCTthe
attributes take part in which rows are equal. An attribute beside an
aggregate recombined across endpoints is refused400
indecomposable-aggregate, and ordering on an attribute stays refused400
endpoint-variable. The501 not-implementedanswer for these queries is
gone.openehr-federation0.0.27 adds theattributemodule with
EndpointAttribute,ColumnSource::Endpoint(EndpointAttribute),
Analysis::attributes,NodeAnswer::with_attributesand
Merged::attributes; the engine addsPlan::annotatingand
FederatedAnswer::attributes. CP-37 is covered. -
Track 10, the adversarial identifier-leakage suite, judged on node-side
wire capture (#90; §16.3 track 10, §5.4, N33, N34, N5, CP-26). One
synthetic patient identifier is supplied in each of the four positions the
track names: anEHR_STATUS.subject.external_refpredicate, a
PARTY_IDENTIFIED/DV_IDENTIFIERpredicate, aSELECTprojection, and
the client's query string or headers (X-Request-Id,Authorization,
Prefer, the targeting and completeness headers, and a free-form header).
Each runs on the fan-out, on a query directed at one node, and on the
single-node route, and the journal of the capturing proxy in front of each
node must hold no occurrence of the identifier, its namespace or any of its
fragments in the path, the query string, a header or the dispatched AQL,
raw or percent-decoded; a refusal must be the400the specification
names, with no node asked. Every dispatched request must locate its node by
the node's ownehr_idalone, and a projected subject column must be the
re-injected identifier. The converse check commits aCOMPOSITIONwith
the identifier in aDV_IDENTIFIERand finds it byte-identical at the
node, compared by bytes and by digest. The gateway's own log (the request
lines, the security events and the panic line) is searched too and must
hold none of it. The suite runs against two mock nodes in the normal suite
and against the two FerroEHR nodes of the harness behindFERROFED_E2E,
and the conformance matrix records track 10 as covered for the gateway's
half; CP-27, the node's own obligation, stays with the node profile (#93).
ferrofed-testkitadds theleakmodule, the search over the proxy
journal. -
The
openEHR-federation-endpointandopenEHR-federation-organisation
request headers, the targeting mechanism beside the AQL (#71; §8.4,
§8.4.1, N35, CP-28). Each carries a comma-separated list of registry ids,
over one field line or several, and selects nodes exactly as the
FROM ENDPOINTandORGANISATIONdirective does: a listed endpoint where
the patient is not known isnot-resolved, every other endpoint is
excluded, an identifier the registry does not know, or a header with no
identifier in it, is a400(endpoint-unknown,organisation-unknown),
and a selection of no endpoint is404 no-destination. A query directed at
one endpoint by the header may be an aggregate, as one directed by the AQL
may. The headers apply to the federated query and to a request routed to
one node, where together they select exactly one endpoint. When the
directive and a header, or the two headers, appear in one request, the
same node set proceeds and different sets are refused400with the new
codetargeting-conflict, whose message names both sets by their registry
endpoint ids; the gateway never merges them and never picks one. No query
parameter targets anything:?endpoint=and?organisation=have no
effect on the federated query, and on a routed request they are refused
400 query-parameter-refusedlike any parameter the operation does not
declare. Neither header reaches a node: both joinAuthorizationand
X-Request-Idin the set a routed request always withholds. -
The registry document in FHIR form (#74; N19, N20, N21, §15.2, CP-13,
CP-20). Withregistry.format = "fhir",registry.documentnames a FHIR R4
JSONBundleofOrganizationandEndpointresources, the shape an mCSD
directory delivers, which loads into the same members and routes
identically to the native TOML form. The registry's ids travel as
identifiers in FerroFED's systems underhttps://ferrofed.eu/fhir/sid/,
and an endpoint'sconnectionTypeisopenehr-rest-queryin
https://ferrofed.eu/fhir/CodeSystem/connection-type.config check
refuses, naming the resource, an endpoint whoseconnectionTypeis
hl7-fhir-rest, an informal string or any other code, an endpoint whose
managing organisation is missing or not in the Bundle, an endpoint no
organisation or two organisations operate, an id that is missing or
repeated, and a node its endpoints disagree on.ihe-iti0.0.8 reads the
directory content under itsmcsdfeature, resolving references inside the
Bundle as FHIR R4 §2.36.4.1 does. -
The
FROM ENDPOINTandORGANISATIONdirective in AQL (#70; §8.1,
§8.4.1, N11, N19, N20, CP-6).FROM ENDPOINT p ["node-a-pub", …]asks
exactly the listed endpoints, andFROM ORGANISATION ["org-a"]asks every
endpoint the registry lists as managed by each organisation. The
identifiers are registry ids, never URLs. The directive does not replace
resolution: each listed endpoint is still asked about its ownehr_id, a
listed one where the patient is not known isnot-resolvedand fails
nothing, and every endpoint the directive does not list isexcluded. The
directive is parsed byopenehr-query'sfederationfeature and never
reaches a node: each node receives the same standard AQL as for the
undirected query. An identifier the registry does not know is refused400
with the new codesendpoint-unknownandorganisation-unknown, which
locate it by its place in the list and never quote it, and an organisation
that manages no endpoint answers404 no-destination. A query directed at
one endpoint dispatches an aggregate or a function AQL does not define
unchanged (N14, §11.6.3). The directive's variable may only be selected, as
an ENDPOINT attribute, which #72 adds to the rows, and any other use is
refused400with the new refusalendpoint-variable.openehr-federationadds
aql::directive::FacadeQuery,Context::with_targeting,
Analysis::sourcesandColumnSource::Endpoint. Golden case 02 now
passes, and CP-6 is covered. The endpoint and organisation headers of §8.4
are #71. -
The registry maps every observed
creating_system_id(#67; §12.2, N21,
the mapping half of CP-13): a[[creating_system]]entry in the registry
document maps acreating_system_idthat is no member's ownsystem_idto
an endpoint, and a member's ownsystem_idmaps to that member without one.
The document is refused, byconfig checktoo and naming the
creating_system_id, when a mapping names an undeclared endpoint, maps one
id twice (ASCII case aside), or maps a member's ownsystem_id.
ferrofed-registryadds the learned map: the first sighting of an id the
document does not route learns a read route to the endpoint it was seen
at, and a learned mapping never overrides the document. A sighting at a
second node, or a learned mapping the document contradicts, withdraws it
and raises an integrity incident, logged atERRORwith a stable kind and
routing ids only. An id nothing routes is a typed miss, never a default
endpoint. The follow-up read routing that consumes the table is #64. -
Opt-in version-identity dedup (#56; §10, N15, N36, CP-9, CP-29): a request
that sendsopenEHR-federation-dedup: version-identitygets one copy of a
version held at several endpoints, keyed on the fullOBJECT_VERSION_ID
of the row'sCOMPOSITION, elseVERSION, uid asopenehr-basereads
it. The copy kept is the one from the endpoint whose registrysystem_id
is the version'screating_system_id, else the one from the lowest
endpoint id. Two versions of one object are two rows, and a row with no
version uid is never suppressed.meta.federation.deduprecords the mode
on every answer,noneand failing424and504envelopes included,
and beside the rowssuppressed_rowsandsuppressed_endpoints[], counted
beforeDISTINCT,OFFSETandLIMIT. Every node is asked the version
uid, as a hidden column when the client does not select it (under
DISTINCTonly a selected uid is the key), and a query with aLIMITand
noORDER BYis ordered on it. Under the mode a tie on theORDER BY
keys breaks on the uid beforeendpoint_id, which keeps a per-node
LIMIT n(ork + nfor a page) exact after suppression (§11.6.1). The
default staysnone(§10.1), whichnonestates explicitly; any other
value, or a repeated header, is400dedup-invalid. A node whose version
uid is not anOBJECT_VERSION_IDisnode-error, and the query fails
424under all-or-nothing. A recombined aggregate under the mode is400
indecomposable-aggregate(§11.6.3).openehr-federation0.0.20 adds
dedup::DedupMode,aql::Context::with_dedup,
order::ResultOrder::with_version_key,merge::NodeAnswer::with_system_id,
merge::Suppressed,merge::Disagreement::VersionIdand
aql::refusal::Indecomposable::Dedup. -
SELECT DISTINCTat the Tier (#55; N13, CP-8, CP-32): a row two nodes
return is answered once, compared on the columns the client selected under
the Tier comparator (2and2.0are one value, two spellings of one
instant are two), and the duplicates are removed before theLIMITand
theOFFSET(AQL 1.1.0 §LIMIT), so a duplicate no longer takes two slots
of aLIMIT nanswer or of a boundedOFFSETpage. The copy kept is the
first underORDER BY, thenendpoint_id. A re-injected subject column
and a column the gateway adds never make two rows distinct. Each
endpoint'srow_countstays what it contributed (§9.5). A node that
returned its fullLIMITwith two rows the Tier holds equal is
node-error, because a distinct row can lie past its cut, and the query
fails424under all-or-nothing.openehr-federation0.0.18 adds
order::ResultOrder::with_distinctanddistinct, and
merge::Disagreement::Distinct. -
Single-node routing for the EHR area (#61; §7a.1, §7a.3, §9.6, §11.2, N22,
N31, N33, CP-24): every ITS-REST operation under{base}/v1/ehr/{ehr_id},
named byopenehr-its'sroutes::lookup, is forwarded once to the node
theopenEHR-federation-endpointheader names, throughClient::forward.
The body reaches the node byte for byte, aDV_IDENTIFIERin a committed
COMPOSITIONincluded, and the node's status, body,LocationandETag
come back unmodified, a404or500included. Every routed answer names
the acting endpoint and its node'ssystem_idin
openEHR-federation-endpointandopenEHR-federation-system-id. Only the
request headers and query parameters the matched ITS-REST operation
declares travel, read fromopenehr-its's per-operation parameter table
(FerroEHR #3530):If-Matchreaches the node on aPUTand never on a
GET. The client'sAuthorizationandX-Request-Idnever travel, and
the node receives the request's mintedX-Request-Id. An undeclared query
parameter, andsubject_idorsubject_namespaceanywhere, is refused
before dispatch. New codes:target-required(a write that names
no node,400),endpoint-unknownandendpoint-several(400),
query-parameter-refused(400),node-timeoutandnode-unreachable
(504) andnode-refused(424, a node that refused the gateway's onward
credentials). A read that names no node still answers501until #62
routes it. A node's3xxis its answer, passed on with itsLocation
unmodified, and no request is re-sent to a host outside the registry. -
Decomposable aggregates (#54; §11.6.3, N14, N39, CP-10, CP-32): an
undirectedCOUNT,SUM,MIN,MAXorAVGis sent to every node and
answered with one recombined row in the client's columns, never one row
per node. Counts and sums add exactly, reals in decimal arithmetic;MIN
andMAXare re-applied over numbers and complete date-times;AVGis
asked of each node as itsSUMandCOUNT. A node value the
recombination cannot use isnode-error, so the query fails424with no
value.DISTINCT,COUNT(DISTINCT …)and a plain column beside an
aggregate are refused400(indecomposable-aggregate), and apartial
request for a recombined aggregate is refused400(partial-aggregate).
The new[federation] decomposable_aggregateslist (all five by default,
[]for none) declares the functions, and an undeclared one is still
undirected-aggregate. A directed single-node aggregate is sent unchanged.
openehr-federation0.0.13 adds theaggregatemodule,
aql::Context::with_decomposable_aggregatesandmerge::combine, and
takesrust_decimal1.43.0, already in the tree throughopenehr-rm. -
The full per-endpoint report (#49; §9.5, §11.1, N16, N40, CP-11, CP-31):
every registry member appears inmeta.federation.endpoints[], an endpoint
a directed request did not name asexcludedwith the reason, which stays
out of scope, so it neither clearscompletenor fails the query. A
[[node]]of the registry document may record its CDRproductand
version, which the report carries only from there and omits when the
registry does not say.latency_msappears exactly for the endpoints the
gateway dispatched to, androw_countcounts what each node contributed
before any federation-levelDISTINCT, dedup orLIMIT. -
Completeness (#50; §11.2 to §11.4, N6, N37, CP-30): all-or-nothing stays
the default, and a request opts into best-effort with
openEHR-federation-completeness: partial. Under best-effort the gateway
answers200with the rows of the nodes that answered, names every other
node with its status, and setscomplete: false. A cross-reference that
cannot answer is reported there and is not a424.allis accepted
explicitly. Any other value, a repeated header, orpartialwhere
federation.best_effort = falsewithdraws the mode is a400that asks no
node and never quotes the value. Thenot-resolvedandconsent-denied
carve-outs and the scope rule hold in both modes.completeis always
derived from the statuses. -
Timeouts (#51; §11.5, N38, CP-31; RFC 7240): a client shortens the budget
withPrefer: wait=<seconds>, and a longer wait leaves the configured
budget in force. The effective budget is the one reported in
meta.federation.timeout, and a wait that set it is echoed in
Preference-Applied. Only the firstwaitcounts; a malformed one is
ignored and never refused.wait=0asks no node and reports each one
time-out. The overall budget runs from the request's arrival, so the
patient resolution and the fan-out share it and the gateway answers inside
it, and abandoning one node never aborts a request in flight to another. -
ORDER BYwithLIMITre-applied at the Tier, with a deterministic
tie-break (#52; §11.6.1, N9, N13, N39, CP-8, CP-32). Every node is sent the
client'sLIMIT nunchanged, with the row's uid appended as the last
ORDER BYkey; anORDER BYpath the query does not select travels as a
hidden column the client never sees. The gateway merges the node answers
under one total order (null greatest, numbers exactly, complete date-times
by instant, strings by code point,DV_ORDEREDvalues through the openEHR
RM's own comparison), breaks ties on the endpoint id and then the uid, and
cuts the result atn. A node that returnednrows out of that order, or
more thann, is reportednode-error, so the query fails424under
all-or-nothing.TOP nis read asLIMIT n;TOP n BACKWARD, aTOPbeside a
LIMITclause, and aDISTINCTquery ordered on a path it does not
select are refused400. A query withLIMITand noORDER BYnow returns
at mostnrows across all nodes. -
OFFSETpaging across a fan-out (#53; §11.6.2, N9, N39, CP-32).OFFSET
never reaches a node. Under the defaultfederation.offset_strategy = "bounded",ORDER BY … LIMIT n OFFSET kasks each node forLIMIT k + n,
checks each node's visible order as forLIMIT n, merges in the federation
order and returns rowsktok + n. A page whosek + nis past
federation.max_offset_window(1000 rows per node by default; 0 refuses to
boot), anOFFSETwith noLIMIT, and anOFFSETwith noORDER BYare
refused400, the first naming the bound.offset_strategy = "reject"
refuses everyOFFSETpast zero400. The ITS-RESToffsetandfetch
members follow the same strategy. -
The
comment-styleguard checks citations (#173). It fails on a Rust
comment, doc comment or lintreasonthat citesdocs/architecture.md, a
path under.claude/or a rule file by name, or that names a
decision-register entry such asdecision A17. It applies the same check to
the full-line#comments of the shell scripts underscripts/and of every
Cargo.toml.comment-style.sh --self-testproves each refused form and
its near misses, and CI runs it before the full-tree pass. Every comment that
cited one of these now cites the specification section it rests on, or says
that no specification governs it. -
The error vocabulary (#57; §11.2, N32, N36, N37, N42, CP-12, CP-30): every
failure the gateway reports on its own behalf answers the ITS-RESTError
body with a stablecodeand therequest_id, and its status follows one
table. A refused query is a400named by its refusal (not-aql,
unreducible,offset-unsupported, and the rest), an unknown path a404
not-found, an unexposed ITS-REST area a501not-implemented, and the
gateway's own fault a500internal, and a request with no member in
scope a404no-destination;ehr-id-collisionand
controlling-system-unreachable(409) are fixed for follow-up routing.
The codes are API and are only ever added. The book's "Errors and status
codes" page lists them all, and a test holds the page to the gateway's
table. -
The
versionsguard holds two hand-typed version facts to their sources
(#181). The landing page's release note and status panel must name the
newest## [x.y.z]release ofCHANGELOG.md, so a release cut that forgets
the page fails. Each specification row ofdocs/VERSIONS.mdmust name a
crate constant (FEDERATION_SPEC,ITS_REST,AQL) that carries the
version the row pins.versions.sh --self-testproves both checks, and CI
runs it before the full pass. -
Generated conformance badges on the README (#175), as shields.io endpoint
files underconformance/badges/: the Gateway points of §17 covered out of
the Gateway total, the Node and Operator point counts, each labelled with the
pinned specification version and linked to the book's conformance page, and
the AQL golden cases passed out of the vendored corpus, linked to its pass
list.scripts/conformance/matrix.sh --badges-writewrites the files and the
README block betweenconformance:beginandconformance:end, and the
conformance-matrixguard fails when either drifts from the matrix or the
pass list. The golden test fails when a case in
conformance/aql-golden/pass-list.txtstops passing or an unlisted case
passes, and rewrites the list whenFERROFED_CONFORMANCE_UPDATEis1.
Case 02 (FROM ENDPOINT) is refused until #70 and is not counted as
passing. The static badge row gains the image-pulls badge for
ghcr.io/ferrohealth/ferrofed. -
The
versionsguard holds the book's "Pinned versions" page to
docs/VERSIONS.md(#198). Each row of its pin table names its matrix rows
by their exact names, and every pin it states, a version, a package, an
edition 2024or an abbreviatedcommit, must be the one those rows pin;
a pin the guard cannot read fails rather than passing unread. The page now
lists all fiveopenehr-*crates, the vendored PIXm and PDQm packages and
the specification's source commit as rows of their own.versions.sh --self-testproves each drift. -
Tests that pin the two optional facilities FerroFED does not offer (#59,
#60; §11.6.4, §11.7, CP-31, CP-32). A query sent withPrefer: respond-async, alone or besidePrefer: wait, gets the ordinary
synchronous answer under the same budget: never a202or a
Content-Location, andPreference-Appliednever namesrespond-async
(RFC 7240 §2, §3). A boundedOFFSETpage carries no cursor handle or
expiry inmeta.federationand runs the fan-out on every request.
Changed
-
A gateway with
registry.documentset now refuses to boot without
federation.id, the federation's own identifier thatOPTIONS {base}/
names (#73; §7a.2, N30). Addid = "<your federation>"to[federation];
an empty id is refused. -
The
openehr-*family moves from 0.0.78 to 0.0.79 (#234). The
openehr-rmattribute model now holds the BASE primitives, theOrdered
marker and the target class of a reference-typed attribute (FerroEHR
#3537), which the AQL rewrite reads to decide which selected paths a node
can order underDISTINCT.openehr-federationis 0.0.28. -
The
openehr-*family moves from 0.0.77 to 0.0.78 (#237), and an onward
credential is checked at configuration load by the node client's own
Authorizationcomposition (FerroEHR #3535), for each endpoint and each
PIX Manager alike. A bearer token must now be theb64tokenof RFC 6750
§2.1 (letters, digits,-,.,_,~,+and/, then any=
padding), so a token holding a space, a quote or any other character
outside that set is refused byconfig checkand at boot with exit code
78, naming its key and never its value; it used to pass and then fail at
the node. The basic rules of RFC 7617 §2 are unchanged.
openehr-federationis 0.0.26. -
The
openehr-*family moves from 0.0.76 to 0.0.77 (#195).openehr-query
now classifies every AQL function call (FerroEHR #3529): a string, numeric,
or date and time function of AQL 1.1.0 is a built-in, and any other name is
not. The rewrite reads that classification where it kept a list of the 16
function names of its own, so an undirected query that calls a function
AQL does not define is refused exactly as before (undefined-function;
§11.6.3, N14), and theCONCAT,CONCAT_WSandSUBSTRINGfolding and
theDISTINCTcut check read the crate's classification too. Every
openehr-itsclient is built with redirects off (FerroEHR #3531), so a
node's3xxanswer is never followed: the node isnode-error. -
Incompleteness is carried by
meta.federation.completealone, and the
gateway emits no FHIROperationOutcome(#58; §9.1, §11.4, N17, CP-12).
§11.4, CP-12 and track 4 ask for the resource only for a FHIR-facing
consumer, and the answer is an ITS-RESTRESULT_SETwhose federation
additions live undermeta.federation. The conformance matrix gives CP-12
that reason beside its scored status codes, a test asserts that a
best-effort200and an all-or-nothing424each carrycomplete: false,
validate againstfederated-result-set.schema.jsonand hold no
OperationOutcome, and the client contract in the book says so. Where the
resource would travel for a gateway with FHIR-facing consumers is recorded
as an upstream report (#204). -
The conformance record's loose ends (#196). The
comment-stylecitation
checks read the conformance tables underconformance/: their#comment
lines and theirreasoncolumn, which the book renders. The tables now
cite §16.2, §16.3, §16.4 and §17 where they cited an internal file or a
decision-register entry, or say that no specification governs the choice.
The CP-10 and CP-32 rows name #187. The book page links the specification
site at the versiondocs/VERSIONS.mdpins, with no second copy of it in
scripts/conformance/matrix.sh.openehr-federation0.0.16 marks its
local list of AQL function names for removal (#195). -
The
comment-stylecitation checks cover the rest of the tree (#178): the
full-line comments of the workflow and composite-action YAML under
.github/, theechoandprintftext of a workflowrun:block,
clippy.tomlwith itsreasonstrings, and the quickstart TOML under
docker/. The per-edit hook runs the guard on every file kind CI checks.
Every comment, lint reason and printed line there that cited an internal
file or a decision-register entry now cites the specification section or
official documentation it rests on, or says that no specification governs
it. The six vendor scripts no longer write an internal path into their
PROVENANCE.md. -
The
comment-stylecitation checks refuse a citation of any markdown file
underdocs/outside the vendoreddocs/specs/tree (#184): a path that
opens a parenthetical or is followed bysectionor§. Naming
docs/VERSIONS.mdas the file a script or test reads still passes. The
checks also read every YAMLdescription:scalar and the trailing#
comment after a YAML or TOML value, outside quoted strings and block
scalars, and the guard runs the same under mawk. The citations this
catches cite the GitHub documentation or say that no specification
governs them, among themfuzz.yml, thesetup-rustaction
description, the version and corpus scripts and the CI rule. The crate
manifests' comment on the 0.0.0 name reservation changes with them, so
openehr-federationmoves to 0.0.15,ihe-itito 0.0.7 and
nl-generic-functionsto 0.0.4 with no change to their code. -
The
comment-stylecitation checks also refuse a citation of a
README.mdof the tree, at the root or in a member, outside the vendored
docs/specs/andvendor/trees (#198), in the same citation form. Naming
a README as a file a script reads still passes.fuzz.ymlcites the
libFuzzer documentation for how a crashing input is kept, and the scripts
that cited a README no longer do. -
The release lane fails a tag whose tree has no root
Cargo.toml(#198),
with a message saying the tag cannot be checked against the workspace
version. It used to skip that check. -
The release lane has one path (#201): the binaries and the image build for
every tagplanaccepts, andfinalize-releasepublishes only when every
build leg succeeded. The branches for a tag with no workspace, whichplan
now refuses, are gone. The book's claims page links to the pinned-versions
page for theopenehr-*pin instead of restating the number, and
docs/ci-cd.mdnames all four self-tests of thetracker-helpersjob,
rel.shamong them. -
scripts/gh/fields.shchecks its arguments before anyghcall (#198).
No argument, an unknown command, a wrong operand count or--helpprints
the usage to stderr and exits 2, with no network call and no token needed.
fields.sh --self-testproves it against a stub that records every call. -
The per-edit
comment-stylehook also reads theconformance/*.tsv
tables (#198), as CI does, and stays a quiet pass for any other file.
Conformance track 5 (Dedup + DISTINCT) names #187 beside its issues, as its
CP-10 and CP-32 rows do, and the book's conformance page is re-rendered. -
Error bodies (#57): the gateway's own refusals (
404,501, a caught
panic's500) answer the ITS-RESTErrorshape withcodeand
request_id, where they named the code in anerrormember, and the codes
are kebab-case (not-found,not-implemented). No error body quotes the
query, a parameter value or a header value (§5.4.3); a424or504under
all-or-nothing stays the §11.4 result set and echoes the client's ownq
(N17). A node row shorter than the dispatched query selects is now that
node'snode-error(424, or reported underpartial), where the whole
query answered502(§11.1, §11.2). -
The
openehr-*family moves from 0.0.74 to 0.0.76 (#57). 0.0.76 keeps the
members an open ITS-REST schema admits in anadditional_propertiesmap
(FerroEHR #3526), so every error body is the generated ITS-RESTError
withcodeandrequest_idin that map, and no FerroFED-side error type
remains. The generated request and result-set types carry the same map. -
The site, the README and the book describe the released gateway (#176).
The landing page opens with the FerroFED mark and the current release, and
shows the quickstart, the release binaries and the image
ghcr.io/ferrohealth/ferrofed, with what each specification property has
in v0.0.3 and what is planned. The README's install section names the
release assets and their verification, and its quickstart runs the
published image withdocker compose up --wait. The book's introduction,
claims, deployment, container and contribution pages, the governance and
contribution guides, and the working rules no longer describe a design
phase or a project with nothing to install. -
The quickstart and the end-to-end harness run two FerroEHR nodes (#155,
decision A44). EHRbase left both, because it refuses a.in
PARTY_REF.namespace, which openEHR BASE admits (#118). Incompose.yaml
the services areferroehr-aandferroehr-b, each on its own database
and with its ownsystem_id(node-a.quickstart.local,
node-b.quickstart.local, which the quickstart registry declares), and
node B moved from port 8091 to 8082 (FERROEHR_B_PORT; node A's
FERROEHR_PORTis nowFERROEHR_A_PORT). Every end-to-end case seeds the
patient's subject on both nodes, and thee2e (containers)CI job now runs
the server's container tests as well as the testkit's. -
The tracker records the kind, the urgency and the size of an issue in
GitHub's native issue type (Bug, Feature, Task) and the organisation's
Priority and Effort fields (#154). Thebug,enhancementandP0to
P3labels are retired, the bug and feature forms set the issue type, and
scripts/gh/fields.shandscripts/gh/migrate-fields.shcarry the model
and the migration.
Fixed
- The local crate-bump hook judges a commit or a push against
origin/main,
the base CI uses, and leaves finding the merge base to
scripts/checks/crate-version-guard.sh(#243). A branch that bumps a crate
to the version main already took is now blocked locally, as CI refuses it,
where it used to pass. The book's checks page lists thetracker-helpers
job, the self-tests of thescripts/ghhelpers, among the tier-1 checks. - The outbound gate no longer reads the host and port of a registry
endpoint URL (#232; §5.4.1, N33, CP-26). An endpoint whose host or port
contained a withheld identifier, such as a short local identifier that
matched a port number, refused every query for that patient. The
authority comes from the operator's registry and never from a request,
and §5.4.1 names the request path, the query string and the headers. The
gate still reads the path, the query and the fragment, raw and
percent-decoded, with the AQL text, the paging and the headers the
gateway adds. - A
SELECT DISTINCTquery withORDER BYandLIMIT(or a bounded
OFFSETpage) that selects a path whose value AQL defines no order for is
refused400(incomparable-distinct-key), where a node could cut among
distinct rows differently on each repeat (#234; §11.6.1, N13, CP-8,
CP-32). AQL orders on data "available to primitives andOrderedtypes"
(AQL master03-syntax §ORDER BY), so a whole RM object (SELECT DISTINCT c), a data value that is not aDV_ORDERED(c/name, aDV_TEXT), the
DATA_VALUEof anELEMENT, a collection and a path the RM does not
resolve are not keys. The rewrite decides it by walking the path through
openehr-rm's static model from the classFROMbinds, asking the
model'sis_primitiveandconforms_to_orderedlookups and following a
reference to the target class the model names (FerroEHR #3537). Under
DISTINCTonly the selected paths to a primitive or aDV_ORDEREDare
pushed asORDER BYkeys, so the same query without aLIMITis answered
and the other paths are compared at the Tier alone. A path to a primitive
value, such asc/name/valueor…/value/magnitude, and an ordered data
value, such asc/context/start_time, are pinned as before. Theaql
feature ofopenehr-federationnow depends onopenehr-rm; the crate is
0.0.28 and addsRefusal::IncomparableDistinctKey. - A query with
ORDER BYandLIMIT(orTOP, thefetchmember, or a
boundedOFFSETpage) ordered on a path whose value AQL defines no order
for is refused400with the new codeincomparable-order-key, where it
used to be sent to every node and re-ordered at the Tier unchecked (#242;
§11.6.1, N39, CP-32). AQL ordering "assumes that data identified by the
path … are comparable", the primitives andOrderedtypes (AQL
master03-syntax §ORDER BY), and §11.6.1 holds the federated firstn
rows in each node's firstnonly "under a total order", so a node's cut
onORDER BY c/name LIMIT 10, aDV_TEXT, could drop a row of the
answer. The rewrite uses the comparability rule of #234, and the
refusal points at the key by position. The same order with noLIMITis
answered: every row reaches the Tier, which orders them all under its own
total order, the same on every repeat. UnderDISTINCTthe refusal stays
incomparable-distinct-key.openehr-federationis 0.0.29 and adds
Refusal::IncomparableOrderKey. - Under version-identity dedup,
SELECT DISTINCTcompares the version uid
without regard to case, as the dedup and the Tier order already do (#234;
§10.2, CP-8, CP-9; BASEmaster05-identification_package.adoc
§"Composite Identifiers and Case"; AQL master03-syntax §DISTINCT). Two
rows of one node whose uids differ only in case are one distinct row, the
first in the Tier order, with its text as the node sent it; a node cut at
itsLIMITthat returns both is refused asnode-error. Outside dedup a
uid compares as sent, in step with the Tier order. - A
SELECT DISTINCTquery withORDER BYandLIMIT(or a bounded
OFFSETpage) whose selected function column the selected paths do not
fix is refused400(unordered-distinct-cut), where a node could cut
among distinct rows tied on every path differently on each repeat (#210;
§11.6.1, CP-8, CP-32). AQL orders only on paths (AQL master03-syntax
§ORDER BY), and underDISTINCTthe gateway adds no column, so the
selected paths are a node's only keys. A call to a single-row function AQL
defines whose arguments are literals, parameters and selected paths, such
asLENGTH(c/name/value)besidec/name/value, is answered as before; a
call that reads a path the query does not select, a clock function such as
NOW(), andTERMINOLOGYare refused. The same query without aLIMIT
is answered.openehr-federation0.0.22 adds
Refusal::UnorderedDistinctCut. - Under version-identity dedup, the Tier orders the version uid without
regard to case (#229; §10.2, §11.6.1, §11.6.2, CP-9, CP-32; BASE
master05-identification_package.adoc§"Composite Identifiers and
Case"). Two copies of one version whose ids differ only in case rank as
one in the merged order and in the check of each node's order, as an
ORDER BYkey and as the tie-break, so anORDER BYwithLIMITand a
page under dedup stay exact for them. Every row keeps its uid as its node
sent it. The comparison isopenehr-base'scomposite_id_key. A node cut
at itsLIMITthat orders uids byte for byte and returns them in an order
the Tier's disagrees with is refused asnode-error, since the Tier
cannot change how a node sorts. Outside dedup the uid orders as before, by
code point.openehr-federationis 0.0.23. - An onward credential the
Authorizationheader cannot carry is refused at
configuration load, byconfig checkand at boot with exit code 78, in
place of failing every query to its endpoint with a500(#231). A bearer
token, inline or read frombearer_token_file, is checked as the
Bearer <token>value the node client sends, by the samehttpparse the
client applies, and a basic user or password holding a control character,
or a basic user holding a colon, is refused as RFC 7617 §2 requires. The
refusal names the key the value came from
(credentials.<endpoint>.bearer_token, its_filesibling,.useror
.password) and never the value. A PIX Manager's
[pixm.manager.credentials]is held to the same rule. - Version-identity dedup compares identifiers without regard to case (#225;
§10.2, CP-9; BASEmaster05-identification_package.adoc§"Composite
Identifiers and Case"). Two copies whose version ids differ only in case
are one version, and the node whosesystem_iddiffers from the version's
creating_system_idonly in case keeps the originating copy. Before, both
were compared byte for byte. A kept row still comes back as its node sent
it. The comparison isopenehr-base'scomposite_ids_equaland
composite_id_key, which the registry identifiers already use, and the
session-scoped resolution bindings now key onEhrIdin place of their
own case fold.openehr-federationis 0.0.21. - A patient query is no longer refused at random when its withheld
identifier is a short hexadecimal value that occurs inside the minted
X-Request-Id(#227; §5.4.1, N33, CP-26). The outbound gate reads the AQL
text, the paging, the URL and every other header the gateway adds, and
skips the minted id, whichOutboundId::mintmakes from no client input.
A request a panic, the request timeout or the body ceiling answers now has
its request-log line, with the status it answered (500,408,413)
under the gateway's id. The "the federated query failed" line carries the
samerequest_idas the request line. The hygiene assertions of the tests
compare the raw bytes a mock node received, so a header value or a body
that is not UTF-8 is searched too, and the CP-26 row of the conformance
matrix names #217. - CP-29 is
plannedagain in the conformance matrix and the gateway badge
(#221): only its visibility half, the dedup record of §10.2 and §10.3, is
built, and its write-routing and409half is #66. The dedup tests carry
CP-9 alone.scripts/checks/crate-version-guard.shrun without its base
ref prints its usage on stderr and exits 2. - The book's "What FerroFED claims" page lists decomposable aggregates (#54)
among what has merged since v0.0.3, and no longer as planned; de-duplication
is what remains planned for v0.0.4 (#211). Themergemodule doc of
openehr-federationis wrapped like the rest of the crate's docs, and
openehr-federationmoves to 0.0.19 with no change to its code. - An
ORDER BYwithLIMITquery whose rows carry no uid, such as one that
selects fromEHRalone, returns the same rows on every repeat (#157;
§11.6.1, §11.6.2, N39, CP-32). Each node is asked to order on a row key
after the client's keys: the uid of the firstCOMPOSITIONorVERSION
as before, else<ehr>/ehr_id/value, else the uid of anEHR_STATUSor
EHR_ACCESS; the key travels as a hidden column and never reaches
columns[]. A node cut atLIMIT n, or at theLIMIT k + nof a bounded
OFFSETpage, then keeps the same tied rows each time, so two pages agree
on the rows tied across their edge. A patient query, scoped to one
ehr_idper node, gets noEHRkey;FOLDERis never a key; a
DISTINCTquery keeps its selected columns as the tie-break and gains no
column. A query with no row key is still answered: the Tier orders the
rows it receives, and which tied rows a node returns is the node's.
openehr-federation0.0.17 carries the change. - A query that calls a function AQL 1.1.0 does not define, such as a
product-specificMEDIAN(x), inSELECTorWHEREis refused400
(undefined-function) when it would reach more than one node, where it was
sent to every node and answered one row per node (#187; §11.6.3, N14, N39,
CP-10, CP-32). The gateway cannot tell whether such a function aggregates,
and per-node aggregate rows are the answer §11.6.3 forbids. Directed to one
endpoint, the query is sent unchanged. The single-row functions AQL defines
(AQL §Functions: the string, numeric, and date and time functions, and
TERMINOLOGY) are still sent to every node as written, and the five
aggregates keep the decomposition rules.openehr-federation0.0.14 adds
aql::refusal::Refusal::UndefinedFunction. - A query that uses
TOPtogether with aLIMITclause is refused400
(top-with-limit), whether or not the two counts agree, because AQL
forbids the pair (#162; AQL §TOP, §LIMIT). ATOPquery sent with the
ITS-RESTfetchmember is refused400(top-with-fetch), because
ITS-REST saysfetch"cannot be combined with AQL-top".TOP nalone is
still read asLIMIT n. - A federated query that leaves no registry member in scope, because every
endpoint isexcluded(suspended by the operator, for example), answers
404and asks no node, where it answered200with emptyrows(#166;
§11.1, §11.2, §11.3). A candidate set that localization left empty is not
that case: every member isnot-localized, and the answer stays200
withcomplete: true(§14.1). A patient who isnot-resolvedat every
member in scope still answers200. - A gateway that federates refuses to boot, and
config checkrefuses the
file, unlessserver.request_timeout_msexceeds
federation.overall_timeout_msby more than one second, the margin kept
for combining the answers (#167; §11.5). The refusal names both keys.
Before, a request timeout between the two let the server's408, with no
body, cut a slow fan-out instead of the504that carries
meta.federation. - The documentation describes the gateway that exists (#163). The
openehr-federationREADME names both subject carriers as resolution
input. The book's client contract and configuration pages name
POST /v1/query/aqlas served, and the_filesecrets paragraph sits
under the configuration file it describes. Code and doc comments cite the
specification sections a decision rests on, or say that no specification
governs it. - The landing page no longer scrolls sideways on a phone narrower than about
380 px (#182). The audience cards and the other card grids take a column
minimum that shrinks to the page width, so a 320 px viewport shows one
full-width column in the light and dark themes. llms.txtdescribes follow-up routing to the owning CDR as planned for
v0.0.5, where it described it as built (#180). The CI log, the guard
scripts, the analyzer configuration and theresearchlabel no longer
describe the repository as being in a design phase.scripts/gh/labels.shprints its usage and exits2on any argument it
does not know,--helpincluded, before it callsgh(#180). It ignored
such an argument and wrote the label taxonomy to the repository. Its
--self-testproves the refusal.- The
ferrofed-enginecrate description names what it holds (dispatch,
fan-out, the budgets, the completeness decision and the outbound
identifier-hygiene gate) and lists follow-up routing on the creating
system id as planned, where it described routing as built (#191). The
workflows, the release checklist, the version pages of the book and the
repository, and the working rules no longer describe the Cargo workspace
as not yet existing, and the book names theopenehr-*pin as 0.0.76 and
the PIXm and PDQm packages as vendored. scripts/gh/rel.shprints its usage and exits2before it callsghon
a usage error: no argument, an unknown command, a wrong operand count, a
flag other than--replace, or--help(#191). It exited1with no
argument,0on--help, and resolved the repository throughgh
first. Its new--self-test, run by the CItracker-helpersjob, proves
each write's endpoint and each refusal.
Security
- The ask-all probe no longer sends a path
ehr_idthat is not a bare UUID
to every member (#259; §5.4.1, §12.5.1, N33, CP-26). The pathehr_idwas
checked only as aHIER_OBJECT_ID, a form that admits a bare national
number as a one-arc ISO OID, so a patient identifier in theehr_idslot
of a read nothing routed was broadcast to the whole federation. A read
whoseehr_idis an ISO OID, an internet id or a UUID with an extension,
and that no targeting header, resolution binding orehr_idindex entry
routes, is now refused with the new codeprobe-requires-uuid(400),
and no member is asked. The sameehr_idwith the endpoint header is
forwarded to the named node alone, and the index then routes it, since a
member may mintehr_ids in a scheme other than UUID (§12b.2, N42a).
The refusal is logged as the security eventehr-id-probe-refused, which
never names theehr_id. Track 10 gains the identifier in theehr_id
slot.ferrofed-registryaddsEhrId::is_uuid, andferrofed-engine's
probe::Probecarries aProbedEhrId, which only a bare UUID converts
to, in place of the client's path segment. - A panic no longer prints its message to stderr (#260; §5.4.3, CP-26).
ferrofed serveused Rust's default panic hook, which printed the
message, and any value formatted into it, to stderr, part of a
deployment's log stream. The binary now installs its own hook before it
serves. The hook writes one fixed line throughtracing, "a thread
panicked", with the source location and, inside a request, the gateway's
request_id, and never the message. It writes nothing to stderr. A
panicking handler still answers500, and its "the request handler
panicked" line is unchanged. Track 10's panic case now runs in a child
process and asserts that its stderr is empty.ferrofed-serveradds
panic::install_hookandrequest_id::serving. - A request routed to one node no longer logs the client's
X-Request-Id
(#62; §5.4.1, §5.4.3, N33, CP-26). The security events of a refused query
parameter or a withheld request, and the failure events of the routed path
and the ask-all probe, named the client's own id, free text that can carry
a patient identifier. Every event of the routed path now names the
gateway's own id, the one its request line records; the client's id stays
in the response and its error body only. - A client's
X-Request-Idno longer reaches any node (#217; §5.4.1, N33,
CP-26). A legal client value was sent to every node of the fan-out as it
came, so a patient identifier written into it passed the outbound gate,
which checks only the identifiers the gateway resolved on. The gateway now
mints its own id, a version 4 UUID, for every request and sends only that,
the same id to every node of one request. The response header and the
error bodies still name the client's own id. The request log, the security
events and the panic line record the gateway's id and never the client's,
with aclient_namedflag on the request line saying whether the client
sent one; a client that sends none gets the gateway's id back. The book
lists every header a node request carries and where its value comes from.
ferrofed-engineaddsoutbound_id::OutboundId, which only
OutboundId::mintmakes, andDispatchOptions::with_request_idand
fanout::fan_outtake one in place of a string.