Repository navigation
Releases: FerroHEALTH/FerroTERM
Release list
0.1.8
Added
- The server sends a usage report to FerroPULSE, operated by Cadasto B.V.: one
report at each start and one a day, with the instance id, the version, the
deployment kind, coarse host size, the loaded code system families, and
latency per operation group. It never carries a code, a URL, or a request
path. It is on by default,FERROTERM_USAGE_REPORT=offstops it, and it is
off in CI and without a writableFERROTERM_STATE_DIR. The compose file
keeps the state on a named/data/statevolume; a plain container sends
nothing until it is given one. What is sent, what
the collector stores (the source IP address and the country derived from
it, for 13 months), and how to have it erased:
https://ferroterm.eu/docs/operate/usage-report.html.
Fixed
- The
ferroterm startingandlisteninglog lines name every served FHIR
version (fhir_versions=r4,r4b,r5,r6,bases=/r4,/r4b,/r5,/r6) instead of
r4balone.
0.1.7
Changed
- The Licensor and copyright holder of the project's own work is Cadasto B.V.
(#700), which replaces Vernum Projecten B.V. EveryLicensor:, copyright and
SPDX-FileCopyrightTextline, the contribution grant, the governance text and
the site footer name the company. The licence terms are unchanged, and
maintainer credit stays a person. - A commercial licence, and any other business or licensing question, goes to
Cadasto B.V. at info@cadasto.com or https://www.cadasto.com/contact/ (#702).
EveryLICENSEcopy, the README, the handbook licensing page,
MAINTAINERS.md,GOVERNANCE.mdandSUPPORT.mdsay so. The maintainer
keeps the technical side, and the licence terms are unchanged. - Pull requests record a user-visible change as a fragment under
changelog.d/, which the release cut assembles into this file (#704). - The
buildservice in the releasecompose.yamlbuilds any code system
ferroterm-buildknows, so a deployment without a SNOMED CT licence builds
LOINC or another system through Compose. It mounts a release directory
(./releases, orFERROTERM_RELEASES_DIR) at/releasesand takes the
builder's own arguments:
docker compose run --rm build --loinc /releases/Loinc_2.82.zip --out /data/index/loinc.
FERROTERM_RF2is gone; a SNOMED CT build passes
--rf2 /releases/<zip> --out /data/index/snomed(#713).
Security
rustlsmoves to 0.23.46, which fixes RUSTSEC-2026-0346, RUSTSEC-2026-0347,
RUSTSEC-2026-0348 and RUSTSEC-2026-0349 in the server and sync service TLS
stack, andzipmoves to 9.0.3 because every 8.x release is yanked (#715).
0.1.6
Added
- The sync lists a service's FHIR API beside its feed (#692). The Nictiz
NTS feed carries only the SNOMED CT binary index, and itsCodeSystem,
ValueSet, andConceptMapresources answer on its FHIR endpoint. A source
that names a FHIR API is searched per type and paged to the end, each
resource becomes an entry the subscription and the replace rule read as they
read the feed, and it goes through the resource lane with the digest of what
arrived recorded, since the API advertises none. The run record carries the
API address, how many resources it listed, each entry's origin, and the
subscribed canonicals no listing carried, which the account could not see. A
CodeSystemlisted withcontentnot-presentis reported, never served.
The NTS add-on lists its FHIR endpoint by default (fhir_api = falsekeeps a
run to the feed).
Fixed
-
scripts/live/nts-feed.shfollows the discovery redirect (#602). The
NTS answers its SMART configuration URL with a301to the Keycloak
discovery document, and the script stopped there with "the discovery
document names no token_endpoint"; it now follows up to three redirects,
as the add-on already did. -
A subscription to a SNOMED CT edition matches the live NTS feed (#691).
Ontoserver identifies an edition release by the bare code system
http://snomed.info/sctand carries the edition in the version URI, so a
subscription listing the edition URI admitted nothing and a run took nothing.
A listed edition now admits a release whose version URI sits under it, the
fixture feed carries the live shape, and a built artifact is held under its
system as well as its edition, so a re-run recognises the release as served.
Changed
- The repository moved to the FerroHEALTH organisation: it lives at
https://github.com/FerroHEALTH/FerroTERM, and releases from this one on
publish the image toghcr.io/ferrohealth/ferrotermand sign their
attestations asFerroHEALTH/FerroTERM. Releases up to v0.1.5 keep their
rubentalstrasigner identity, and the book's verification page says how to
check them. - The NTS add-on names the Dutch canonicals the service serves (#602):
ICD-10-NL, the Labcodeset supplement and its three maps, NHG-Tabel 24, the
NHG table prefix, and the zib value set prefix, read from the live service.
The book's NTS section records what the feed and the FHIR API carry, the
24 hour token lifetimes, and how an unlicensed system is hidden.
0.1.5
Added
-
The project extensions resolve on the website (#671). Each of
terminology-artifact,implicit-value-setandoperation-levelunder
https://ferroterm.eu/fhir/StructureDefinition/has aStructureDefinition
in the R4B shape (<name>.json, with its context, its sub-extensions and
their types) and a page at the canonical itself that links it, as FHIR asks
of an extension's URL (https://hl7.org/fhir/R4B/extensibility.html#defining).
The book's FHIR API page links each definition beside its wire example, and
scripts/checks/extension-definitions.shfails CI when the server emits a
canonical the site does not define. -
metadata?mode=terminologydeclares the implicit value sets each served
version resolves (#657). Every form is an extension
https://ferroterm.eu/fhir/StructureDefinition/implicit-value-seton
codeSystem.version, on R4, R4B, R5 and R6, with apattern(the URL
template as the code system's page spells it, placeholders in square
brackets) and anargument(none,codeorexpression). SNOMED CT
declares its five?fhir_vsforms, LOINC/vs,/vs/[id]and
/vs/[partcode], RxNorm/vs, UCUM/vsand/vs/[expression], and an
ICD-11 linearization[entity]/postcoordinationScale/[axis]; the ClaML
classifications, BCP 13, BCP 47 andCodeSystemresources declare none. The
composer's value set picker offers each declared form for the clause's code
system and fills its placeholders, a code through the concept search and an
expression through a text field. -
The history interaction on the persisted resources (#667).
GET {type}/{id}/_historyandGET {type}/_historyanswer ahistory
Bundleon every served version, newest first, withentry.requestnaming
thePOST,PUTorDELETEthat made each version andentry.response
its status,etagandlastModified; the delete is an entry with no
resource (https://hl7.org/fhir/R4B/http.html#history)._sincenarrows
both levels, and_count,_atand_listanswer400not-supported. A
loaded resource answers an empty history. TheCapabilityStatementdeclares
history-instanceandhistory-typewherever it declaresvread. A delete
is now recorded as a version of its own: a version read of it answers410,
and a resource written again after a delete counts on from it, where it used
to start again at version 1 and overwrite the first version in the store.
The viewer's history screen keys a delete by the version itsetagnames. -
_summaryon read and search, and_elementson read (#669). The read
of aCodeSystem,ValueSetorConceptMaptakes_elements, with the
projection andSUBSETTEDtag a search already gave. Both interactions take
_summary(https://hl7.org/fhir/R4B/search.html#summary):truekeeps
id,metaand the top-level elements the served version's definition
marksisSummary;textkeeps
text,id,metaand the top-level elements the served version's
definition makes mandatory, read from the element table of the pinned
packages;datadropstext;falseis the whole resource;count, on a
search only, answerstotalwith no entries. A resource that lost an element
carriesSUBSETTED. A value the search-summary code system does not define
is a400. The
CapabilityStatementdeclares_summarybeside_elements. -
A refused expression constraint states where the parser stopped, as
data (#656). WhenValueSet/$expandorValueSet/$validate-coderefuses
a malformed filter value orecl/implicit value set, the
OperationOutcomeissue names the input inissue.expression
(Parameters.parameter[i].resource.compose.include[j].filter[k].valuefor a
filter of an inline value set,Parameters.parameter[i].valueUrifor a
urlin a body,http.urlfor theurlquery parameter) and carries the
operationoutcome-issue-line(1) andoperationoutcome-issue-col
extensions. The column is 1-based and counts characters into the value
issue.expressionnames; forhttp.urlit counts into the query value as
the client sent it, percent-encoding included. The answer is the same on R4,
R4B, R5 and R6, and never sets theissue.locationelement R6 dropped. The
viewer's composer marks the character from these two, and no longer reads a
number out of the diagnostic's wording. -
The viewer's editor bundle reads a resource's versions, compares two of
them, and restores one (#637). Every authoring screen links to
/ui/editor/historyfor the resource it has open. The version list is read
with whichever interaction the server'sCapabilityStatementdeclares, the
history interaction or the version read, and the screen says which it used
and when the server declares neither. Any two versions can be compared, as
the elements they disagree about rather than as text, with the two server-
assignedmetaelements left out; the pair is in the address, so a
comparison is a link. Restore this version writes that version's whole
resource back withIf-Matchof the version the server holds now, so a
concurrent edit is refused with 412 and shown as one; it needs the write
scope for the resource type, and a reader sees the versions and no restore
control. Where a deployment puts the synchronisation service's admin listener
behind the server's own origin, the screen also lists what the newest run
found about the open resource; where nothing answers there, it shows nothing. -
The viewer's editor bundle composes a local
ValueSet(#635).
/ui/editor/composebuilds aValueSet.composeout of value sets the server
already publishes, own codes picked through the concept search, the filter
properties and operators the served code system version declares, and
excludes. The composition rules are drawn as they apply, a clause that breaks
vsd-1,vsd-2orvsd-3says so by number, and the preview is
ValueSet/$expandbyPOSTwith the unsaved definition inline. Saving needs
user/ValueSet.cudand goes through the REST API withIf-Match, carrying
back every element the form does not draw. Content the server serves from its
loaded indexes opens read-only. -
A concept map is authored in the browser, at
/ui/editor/conceptmap(#636).
The screen carries the metadata, the value sets that scope the map, the
groups of systems it maps between, and every code with the targets it maps
to. The element a save writes is the served version's own: R4 and R4B take
source[x]/target[x]andgroup.element.target.equivalence, R5 and R6
takesourceScope[x]/targetScope[x]andtarget.relationship, R5 made a
group's system a canonical carrying its own version, and R5 added
element.noMap(https://hl7.org/fhir/R4B/conceptmap.html,
https://hl7.org/fhir/R5/conceptmap.html). The relationship control offers
what the served root expands that version's own value set to, and the form
refuses what the version refuses: a target with no relationship, a code that
both maps to nothing and carries a target (cmd-4), and an uncommented
target on the relationship codescmd-1names, which differ per version and
are a warning rather than an error on the R6 ballot. Every code on both sides
is picked out of the system its group names, through the same
ValueSet/$expandsearch the concept browser runs, so a target arrives with
the display the server gave it. A preview runsConceptMap/$translateby
POSTwith the map itself in theconceptMapparameter, which is how a map
that has never been saved is translated through; a server may refuse a map
sent that way
(https://hl7.org/fhir/R4B/conceptmap-operation-translate.html), so the
screen then runs against the saved map and says which it used. Saving sends
the whole resource withIf-Match, a412is shown as a concurrent edit
with a reload, and every refusal renders the server's ownOperationOutcome
and announces its text. A map this deployment loaded opens the same screen
read-only, because the server states nometa.versionIdfor it. -
A code system is authored in the browser, at
/ui/editor/codesystem(#634).
The screen carries the metadata (url,version,status,content,
caseSensitive), the properties the system declares, and the concepts with
their designations and property values, with every coded control offering
what the served root expands that element's own value set to. A concept is
retired rather than deleted: the lifecycle control writes exactly the
standard concept properties the state implies (statusalways,inactive
where the state means it, anddeprecationDateorretirementDate,
https://hl7.org/fhir/R5/codesystem-concept-properties.html), and after a
save the screen runsCodeSystem/$validate-codeon what it retired and shows
the answer. Every write sends the whole resource withIf-Match, so a change
made elsewhere is refused with412and offered a reload rather than
overwritten, and every refusal renders the server's ownOperationOutcome
and announces its text in the screen's live region. A code system this
deployment built from a release opens the same screen read-only, because the
capability statement marks it as served from an artifact. -
The viewer ships as two bundles, and the server serves both (#634). The
reader bundle is what/uihas always served, unchanged in size and in
content. The editor bundle is the same crate built with--features editor,
served at/ui/editor, carrying every reading screen plus the authoring
screens. A reader who never edits downloads no authoring byte, and a person
who edits opens/ui/editor, signs in there, and reads there too. Each
bundle has its own recorded size (app/ferroterm-viewer/bundle-size.jsonand
bundle-size-editor.json), and the release lane embed...
0.1.4
Added
app/ferroterm-sync, the synchronisation service: one way from a terminology
syndication feed into a running server (#579). It runs beside the server and
never inside it. On a schedule (every = "24h",at = "03:00"in a
configured zone, no cron), onPOST /run, or asferroterm-sync run-once,
it lists each configured source, takes what the deployment does not hold yet,
and puts it through one of two lanes: an RF2 archive is built by
ferroterm-buildinto a staging directory and renamed into the index root as
a new release beside the previous one, and a FHIR resource is written into
the managed resource directory with its service's corrections applied. It
then asks the server to reload.activation = "manual"stops after staging
and waits forPOST /activate. Retention keeps the newest releases per code
system and removes the rest only after a reload succeeded; nothing else is
ever deleted, and the server's own write store is never touched. A build that
fails or a reload the server refuses leaves the served set exactly as it was,
rolling back what the run had moved, and still fires the webhook. Every run
writes one JSON record (what the feed offered, what was taken, what was left
behind and why, the builds, the activation, the reload replies, retention,
and the errors), served with the metrics and the manual triggers on the
service's own admin listener. The Nictiz NTS add-on is registered as the
ntssource kind.docker/Dockerfile.synccarries the service and the
builder on the same distroless base, andcompose.sync.yamlruns it beside
the server over one volume with the server's admin listener turned on.- The sync service revalidates the deployment's own content after a release is
activated (#583). Withfhir_base_urlset it reads every locally authored
ValueSetandConceptMapover the server's public FHIR API, with
ValueSet/$validate-codeandValueSet/$expandand theiractiveOnly
parameter, and writes into the run record every local code the new release
made inactive, removed, or left outside the value set it was included
through, each with its system, its code, and the release that changed it. A
run that finds nothing says so. The finding count travels in the webhook
summary, and/metricscarriesferroterm_sync_revalidation_findingsas a
gauge per locally authored resource. The check reports and never edits local
content. - Optional SMART App Launch bearer validation over the write routes and the
admin listener (#586).FERROTERM_OIDC_ISSUERturns it on: the server reads
the issuer's OpenID Connect discovery document and its JWKS before it binds,
refuses to start with the reason when either does not answer, and then
requires an OAuth 2.0 bearer token onPOST,PUT, andDELETEof
CodeSystem,ValueSet, andConceptMapon every served FHIR version, on
POST [base]/$closure, and on every admin-listener route. The scope for a
route is the SMART version 2
one (system/CodeSystem.cto create,.uto update,.dto delete, the
combinedcudandcrudsforms accepted), and the version 1.writeand
.*forms are accepted as the specification maps them
(https://hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html);
FERROTERM_OIDC_ADMIN_SCOPEnames the scope the admin listener requires. A
request with no token answers401with aWWW-Authenticate: Bearer
challenge, a token that does not verify answers401with
error="invalid_token", and a token without the scope answers403with
error="insufficient_scope", each with anOperationOutcome(RFC 6750 §3).
The signature is checked against the issuer's JWKS with an asymmetric
algorithm only, andiss,exp, andnbfare required, as isaudwhen
FERROTERM_OIDC_AUDIENCEnames one; atypheader that names a kind other
than an access token is refused (RFC 8725 §3.1, §3.8, §3.9, §3.12). A token
naming a key the server has not read refreshes the key set, at most once a
minute. The issuer URL ishttps, and a cleartext one is refused unless its
host is the loopback.
[base]/.well-known/smart-configurationis served per version from the
issuer's document, and the capability statements declareSMART-on-FHIRwith
theoauth-urisextension
(https://hl7.org/fhir/smart-app-launch/conformance.html). The read surface
stays open, and a deployment that names no issuer behaves exactly as before. - The server reloads the served set without a restart (#578).
SIGHUPand
POST /reloadboth make it readFERROTERM_INDEXandFERROTERM_CODESYSTEMS
again, open everything read-only, and swap the whole registry; a request in
flight finishes on the set it started on.POST /reloadis served by an admin
listener bound toFERROTERM_ADMIN_LISTEN, which serves nothing else and is
absent when the variable is unset; the FHIR listener answers/reloadwith
thenot-foundOperationOutcomeany unknown path answers. A reload that
meets an artifact it cannot open leaves the served set untouched, logs the
reason, and answers the admin request with a500naming it.
FERROTERM_RESOURCESis carried over rather than reopened./metricsgains
ferroterm_reloads_total{outcome="ok"|"failed"}, and
ferroterm_code_system_loadednow describes the set being served rather than
the set loaded at start. - A concept of a
CodeSystemresource the server loaded or a client persisted
is inactive when its standard concept properties say so (#584).inactive = true, astatusofretired, and aretirementDatethe request is behind
each retire the concept;deprecated, adeprecationDate, and astatusof
deprecatedleave it active, because the specification says a concept that is
deprecated but not inactive can still be used
(https://hl7.org/fhir/R5/codesystem-concept-properties.html).$expandwith
activeOnly=truethen leaves the concept out andactiveOnly=falselists it
withcontains.inactive = true,$validate-codeanswersresult = truewith
inactive = true, and$lookupreturns the properties as written. The R5
property set is data on every version prefix, so the answers match on R4, R4B,
R5, and R6. Where two markers disagree the one that says inactive wins, a
retirementDatethat is not a readabledateTimereads as a retirement
already in force, and thestatusoutput of$validate-codereports a status
only where the resource stated one. crates/terminology-syndication, a client for the Atom syndication dialect
national terminology services publish (#580). It reads a feed with the NCTS Atom
Syndication Format extensions into a typed model, keeps a category term it
does not name rather than dropping it, decides what a run takes (snapshot
only, the binary index refused by name, Ontoserver's newer-date replace
rule), and streams one content item to a file with its SHA-256 or MD5
verified before it counts as fetched. TheSourcetrait is the seam a
service add-on implements, with the listing and the download authenticating
separately. The crate names no country, operator, or code system.- A path in
FERROTERM_INDEXmay name an index root as well as a single
artifact (#592). A path holding nomanifest.jsonis a root, and every child
directory that holds one is opened as an artifact. The server lists the roots
again on every reload, so a release directory that appeared is opened and one
that disappeared is dropped, without a restart. A child without a manifest is
passed over, which makes the stage-then-rename step a synchronisation job uses
race-free; a child whose manifest the server cannot read refuses the reload
and leaves the served set answering. Two releases of one system under a root
are both served, and the greatest version stays the default. scripts/checks/addon-boundary.sh, the guard that keepsaddons/*optional
(#582). A source add-on is code the sync service compiles in and nothing else
links, so the check reads the declared workspace edges fromcargo metadata
and fails when a member outsideapp/ferroterm-syncdepends on an add-on, or
when an add-on depends on a workspace crate other than
crates/terminology-syndicationand the leaf crates it names by hand. A dev
dependency is test scaffolding and stays legal.--self-testjudges synthetic
metadata, so a tree whose every real edge is legal still shows the guard
bites. It runs in CI beside the viewer boundary, and
scripts/checks/comment-style.sh --allnow runs there as a job of its own
rather than only in the per-edit hook.addons/nts, the Nationale Terminologie Server source add-on and the first
member of the newaddons/*tree (#581). It reads the token endpoint from the
service's SMART configuration document, logs in with the documented password
grant (or the client-credentials grant when a client secret is configured),
refreshes the access token inside a one-minute margin, and logs in again from
the stored credentials when the refresh is refused, so an unattended run on
any day needs no person. Credentials come from a file or the environment and
never from the configuration body, and no rendering of the configuration, the
credentials, or the source prints a secret. The subscription names systems by
canonical identifier: an entry outside it is reported as skipped, and a system
the service offers only as Ontoserver's binary index is reported as not
syndicable. A FHIR resource file whoseexperimentalelement is the string
"true"or"false"gets the boolean FHIR declares, with the correction
returned beside the file; a file that needs nothing lands byte-identical. The
add-on is verified against fixtures only until an account exists.
Changed
fhir-types0.1.105 (#611). The generated codec now refuses a primitive
outside its lexical...
0.1.3
Added
ferroterm healthcheck, a subcommand that sends oneGET /healthto the
server's own listen address and exits 0 on200 OK, 1 otherwise with the
reason on stderr. The distroless image has no shell or HTTP client, so this
is what its newHEALTHCHECKruns, and the shippedcompose.yamlstates the
same block:docker compose up --waitnow waits for a serving instance, and
another service sequences itself after the server with
depends_on: ferroterm: condition: service_healthy, as the shippedproxy
service now does. Because the server binds only after every artifact is
open, a passing probe means the deployment answers terminology requests
(#565).
Changed
- The FHIR model now comes from crates.io.
fhir-typesand the generator that
emits it, with the five vendored HL7 packages behind it, moved to the
FerroBRIDGE repository, which publishes the crate; FerroTERM depends on it by
version like any other dependency (#300). Consumers of the crate are
unaffected: the same crate line continues on crates.io, published from
FerroBRIDGE, and the published 0.1.98 source is byte for byte what this
repository generated at 0.1.97. What leaves here is the generator, its
vendored input, thecodegen-driftCI job, and thefhir-typesentry of the
publish order. What replaces the drift job is a pin check:
scripts/checks/versions.shfails when the requirement inCargo.toml, the
row indocs/VERSIONS.md, and the version inCargo.lockdisagree, so a new
release reaches the build only in a change that records it, and Dependabot
opens that pull request on its own.
0.1.2
Added
-
The viewer has a design system, and every screen is drawn from it. Colour is named by role on
:rootand redefined once under:root.dark, so a theme decision is never taken per element; type is six steps, spacing four on an 8-point grid, and motion one duration. A screen names a role and a step and nothing else, whichscripts/checks/viewer-tokens.shenforces as its own CI job: a palette entry, a per-elementdark:variant, a raw type size, an off-scale spacing value, or a bracketed value fails the build. The point is measurement rather than tidiness: a pairing defined once is checked once, and the WCAG 2.2 AA contrast pass covers every pairing the viewer draws in both themes and both densities. -
A command bar sits above every screen, and the navigation is four labelled groups. One field takes a code, a canonical, or free text, decides which it is from the string's shape before any request, and offers what this root can do with it; every offer is a real link, so the keyboard contract is the browser's own and an offer can be shared. The screens are grouped by what a reader came to do rather than by resource type.
-
Every runner picks its code system rather than waiting for a canonical to be typed.
GET /{version}/{type}?_elements=url,name,title,versionis about four kilobytes where the whole search is a quarter of a megabyte, so each control offers what this root publishes, the name leading and the canonical as the value, with the versions beside it being the ones that canonical was published in. A canonical the root does not publish is still typed, and the control opens on the text field for one rather than snapping back to a list it is not in. The screens are named for the question rather than the operation that answers it: Check a code, List a value set, Map a code. -
The overview reads a twenty-system deployment on one screen. It is one table rather than a card each, sortable by any system-level column, each row leading with the name its code system was published under and the canonical beside it, quieter and still selectable. Absence is a mark with the sentence behind it rather than a sentence in every cell. The top bar and the sidebar stay put while the screen scrolls under them.
-
_elementsis answered on every served version. The parameter is defined by every release this server speaks (http://hl7.org/fhir/SearchParameter/Resource-elements) and was refused as unsupported; a search now returns the elements it names plus the ones every resource keeps, and marks what it sendsSUBSETTEDso a client cannot store a subset back as the whole resource. Reading eleven code systems by name costs 4 KB where the whole search costs 255 KB. -
SNOMED CT's
normalFormandnormalFormTerseare generated and served. Both are the Necessary Normal Form of a concept, which the FHIR SNOMED CT page defines and does not say how to produce. The generation is a rendering rather than a computation: the necessary normal form of a precoordinated concept is what the release's inferred view already holds (the SNOMED CT Release File Specification, Appendix D), so the focus concepts are the concept's inferredis aparents and the attributes are its other inferred rows, and no reasoner and no supertype walk runs. The syntax is the Compositional Grammar specification §5, the definition status prefix its §6.7:===where the RF2definitionStatusIdsays the concept is sufficiently defined,<<<where it does not, both always written because the grammar assumes===when none is. Three shapes are this server's own, because no specification fixes them, and each is recorded: the order of what is rendered, which term a reference carries, and the layout. A request naming no property is answered as before, which is what "the server chooses what to return" admits. -
An openEHR archetype's local terminology is served through the ordinary operations. An archetype constrains most of its coded fields with an archetype-local
at-code list, and nothing publishes those as FHIR resources. The scope decision is recorded: this server ingests the resources a producer derives from an archetype and reads no openEHR artefact, so it keeps no openEHR dependency. Theat-codes as aCodeSystem, anac-code as aValueSet, andterm_bindingsas aConceptMapload throughFERROTERM_CODESYSTEMSlike any other supplied resource, and$lookup,$expand,$validate-codeand$translateanswer over them with no openEHR-specific request shape. No openEHR specification defines a canonical URI for a local value set, so whoever produces the resources mints the URL from the archetype id, and this server reads it as an opaque canonical: two deployers minting the same archetype under their own domains are served side by side, and two directories naming one canonical at one version refuse to load rather than picking a winner. -
The brand tokens state what each is safe for. Three of them are safe for the mark and not for text, and nothing said so: teal reaches 3.58:1 on the light surface, which clears the 3:1 a graphic needs and falls short of the 4.5:1 a label needs. Each token carries its measured ratio against both grounds,
--ferroterm-brand-textis the brand colour as text (deep teal on light, teal on the tile, each at 4.5:1 or better), andscripts/checks/brand-contrast.shfails when a token's own comment or the published table stops matching what the value measures. The mark is unchanged.
Fixed
-
An unresolvable
exclude.valueSetis named rather than passed over.ValueSet/$validate-codewalked the excludes only after an include had held the code, so an exclude naming a value set this server does not hold was never reached and the answer was a silentresult = falseover a definition it could not fully evaluate. The excludes are walked either way now, which changes what is reported and no answer: an exclude removes codes, so afalseit could not evaluate was never going to become atrue. The terminology ecosystem settles the reached case as an answer rather than a refusal, with the canonical named in an issue oftx-issue-type#not-found, and it requires$expandand$validate-codeto differ in shape over one such value set; a refusal stays reserved for a definition that cannot be processed at all, which is a cycle. -
A cycle names the value set that closed it. The reference chain pushed the root as
url|versionand every reference as the bare reference string, so the two spellings could never match: a value set referencing itself was caught one hop late, and a two-hop loop was reported under the value set it reached rather than the one that closed it. One rule now decides whether a reference re-enters a chain entry, comparing the url and the versions where both name one, because a reference naming no version means the version the server resolves to. -
check-system-versionis compared against the version an include resolved to. The check ran against the version or pattern a value set wrote, before that version had been resolved against what the server serves, so a value set naming1.x.xwas reported as failing a1.0.xcheck under the pattern's own spelling rather than under the1.2.0it selects, and a value set naming a version nothing serves was reported as failing the check rather than as naming a version that does not exist. The pin now applies the defaults and the forces alone and the checks run over the versions the expansion actually used, so a version that resolves to nothing fails as unknown first, which is what it is. An answer that says a version is unknown also names the versions the server does hold.
0.1.1
Added
-
The viewer runs
$validate-codeand$subsumes./ui/validatevalidates one code against a code system or a value set, and asks how two codes of one system relate. What the form offers is the root's own capability statement: a resource type it does not declare$validate-codeon is not offered, and where a root declares the instance level a stored resource's id stands in for the canonical. The answer states the verdict, the message the server gave, the display the system holds, the version it validated in, and whether the concept is inactive, with every issue the server itemised and the codes beside its text. Both runners put their parameters in the address, so a case is a link you can share. -
The viewer compares the four served roots.
/ui/versionsreadsGET /{version}/metadatafrom/r4,/r4b,/r5, and/r6and draws two tables: what each root says about itself, and which of the terminology operations it declares and at which levels. The screen exists because the four genuinely differ; R4 and R4B declare$lookupat the type level and R5 added the instance level, and the viewer offers on every other screen only what the root you are on declares. Each root's ownoperation.documentationis drawn below, which is where a release states what it takes beyond its ownOperationDefinition. -
The viewer carries the conformance and benchmark evidence this build ships.
/ui/evidenceis the one screen that asks the server nothing: every figure was read out of a committed file by the build script and names that file on the page, so a reader who doubts a number opens it in the repository. It draws the HL7 terminology ecosystem suite mode by mode with the cases each passed of the cases it ran, the latency bars the project claims with the run recorded against each, and the newest committed benchmark run system by system. The figures describe the FerroTERM build the bundle came from, never the deployment answering the page, and the screen says so. -
The viewer's screens sit in a left sidebar, each with its own icon, and the FHIR version selector stays in the top bar. The selector changes what every screen reads rather than moving you to a screen of its own, which is why it is not in the list. On a narrow window the sidebar folds behind a button. The icons are the viewer's own, drawn as inline SVG with no icon package in the bundle.
-
The FHIR core code systems and value sets the specification defines are served without configuration. A deployment that loads no artifact still answers
$lookup,$validate-code, and$expandfor the code systems FHIR publishes itself, so a resource bound toadministrative-genderorpublication-statusvalidates against the version of FHIR it was written for. -
Every ISO 3166 code form is a code of
urn:iso:std:iso:3166. FHIR selects all three from it with acoderegex per form,[A-Z]{2}forValueSet/iso3166-1-2,[A-Z]{3}for-1-3, and[0-9]{3}for-1-N, so an alpha-3 or numeric country code the specification binds has to validate. Only the alpha-2 form was a code, andNLDand528were refused against a system that holdsNL. Each territory is three concepts now, sharing a display and carrying all three forms as properties. -
The viewer browses the concepts of one code system.
/ui/browsesearches a system, reads one concept withCodeSystem/$lookup, and walks the hierarchy the served version declares. What the screen offers is read fromTerminologyCapabilitiesalone. A version that declares a filter carryingchild-of, the operator FHIR defines as "all concept ids that have a direct parent that matches", gets the taxonomy tree on the property that filter is declared on; a version that declares other hierarchy operators, or none at all, gets the flat searchable list with a sentence naming what it does declare. Every search and every level of the tree is aPOST ValueSet/$expandwhose value set travels inline as the operation's ownvalueSetinput, so the screen names no implicit canonical and browses any served system the same way; each request asks forexcludeNestedso one level arrives as one list. The tree follows the ARIA tree view pattern: one tab stop that follows the browser's own focus, the arrow keys walking and opening,Enterselecting, and each row carrying its level, its position among its siblings, and whether it is expanded. A row is keyed by the path of codes above it, so one concept sitting under two parents draws two rows that never share state, exactly one of them is announced as selected, and an answer that loops back stops at the repeat. A node whose children the server counts higher than it listed says so on the node, so a level cut short by the page size never reads as the whole of it. Every pane renders the request its answer belongs to rather than the address as it stands, so a code is never shown paired with the concept that was read before it. The concept pane draws every designation and property the server answered, and links the parents and children it declares. Every parameter lives in the address, so a concept you are reading, the filter you typed, and the nodes you opened are all one link you can share, and a refusal renders the server's ownOperationOutcome. -
The viewer lists the concept maps a root holds and translates codes with them.
/ui/conceptmapsreadsGET /{version}/ConceptMapthe way the value set screen reads its own type, draws every fact a map's publisher declared, and lists the groups it maps with the code count of each. The scope elements are read under both spellings, because R4 and R4B carrysource[x]andtarget[x]and R5 renamed themsourceScope[x]andtargetScope[x]. The$translaterunner below appears only where the root'sCapabilityStatementdeclarestranslateonConceptMap, so the screen never offers a run the server then refuses. Every parameter is sent under the name that FHIR version's ownOperationDefinitiondeclares: R4 and R4B takecode,system,version, andtargetsystem, R5 renamed the code tosourceCodeand the target system totargetSystem, and the R6 ballot renamedsystemandversiontosourceSystemandsourceVersion. The answer is read the same way: a match's relation is labelledEquivalencewhere R4 and R4B answered one andRelationshipwhere R5 and the R6 ballot did, over two different sets of codes, so one version's answer is never drawn as another's. Each match also draws the targetCoding, the map it came from, the source concept and comments, and anyproduct,dependsOn, orpropertythe answer carried. Matches keep the server's own order, which is the edition's concept order for a reverse translation. Every parameter of a run lives in the address, so a translation is a link you can share, and a refusal renders the server's ownOperationOutcome. -
The viewer lists the value sets a root holds.
/ui/valuesetsreadsGET /{version}/ValueSet, filtered by theurlandversionsearch parameters, and reads one of them by id. The filter, the page, and the resource you opened all live in the address, so a list is a link you can share and the back button walks it. This server answers a search in onesearchsetand pages none of it, so the pages the screen walks are its own over the answer that arrived, and it says so. A value set you open draws every fact its publisher declared, the clauses itscomposeselects with, and a link that opens its canonical in the expansion runner. A resource that carries no id says so rather than drawing a link that would answer nothing, and a refusal renders the server's ownOperationOutcome. The screen only reads: the server answers create, update, and delete on this resource type and the viewer calls none of them. -
The viewer runs
$expandand walks its pages./ui/expandtakes a value set canonical withfilter,count,offset,displayLanguage,activeOnly, andincludeDesignations, sends each one only when you set it, and draws the concepts with their designations,expansion.total, and the parameters the server says it applied. Every parameter lives in the address, so a page is a link you can share and the back button walks the run; an implicit canonical carrying its own query string is percent-encoded into the one parameter it belongs to. An expansion the server marks withvalueset-unclosedsays so above the table, in the server's own words where it states a reason, because a code missing from an unclosed list is not a code the server rejects. A refusal renders theOperationOutcomeverbatim, with the codes ofissue.detailsbeside the text, and atoo-costlyrefusal adds what to do about it: ask for a page size and the runner walks the selection instead. The screen names no code system. -
The viewer's first screen answers what a deployment loaded.
/uireadsGET /{version}/metadata?mode=terminologyand draws one card per code system: the canonical, every served version with the default marked, whether the server reads the system's compositional grammar, the designation languages, the declared filters with their operators, the artifact the version was read from with the release the offline build recorded, the content mode where the FHIR version has that element, and whether subsumption is supported. R4 and R4B declare nocodeSystem.content, so the card says the version does not carry it rather than showing nothing, and the R6 ballot'sversion.valueis read where R4, R4B, and R5 useversion.code. A system that declares no version, no language, or no filter states the absence, and a system read from no artifact, such as a registry the server carries, says that rather than reading as a gap. The screen names no code system: every card is the capability statement rendered, so a system this server has never served draws with no change to the viewer, and a test proves it by feeding a document naming an invented system. Each section dis...
0.1.0
Added
FERROTERM_BASE_URL: the URL clients reach the server at, stated per version asCapabilityStatement.implementation.urland in the terminology capabilities. A server behind a reverse proxy answers on an address it never sees, so the endpoint is configured rather than guessed from a socket or a forgeable header; a deployment that names none states no URL.compose.yamlgains aproxiedprofile: Caddy terminates TLS in front and the server publishes no host port.GET /metrics: the Prometheus exposition of the requests answered (a counter and a duration histogram per method, matched route, and status) and the code system versions loaded (one gauge each). The route is off the FHIR base path, so a scrape is never a terminology request, and the labels name the matched route rather than the URI, so the series count stays bounded whatever codes clients ask about.X-Request-Idon every response: the client's own value when it sent a usable one, a fresh UUID otherwise, and the same id in the request's log line. An id that is empty, over 128 characters, or not printable ASCII is replaced rather than echoed.$batch-validate-codeonValueSetandCodeSystem, on every served version: onePOSTcarries the shared inputs once, avalidationparameter per validation, each aParametersof that validation's own inputs, and the answer repeatsvalidationin the same order with that validation's$validate-codeoutputs. A validation states its own value for an input the request also states, and a validation the server cannot run answers anOperationOutcomein its own slot while the others still answer. NoOperationDefinitiondeclares the operation anywhere, so the contract is the terminology ecosystem suite's cases, whosebatch/batch-validatenow passes on/r4b,/r4and/r5.- The fuzz targets under
fuzz/: every parser an untrusted input reaches (ECL, the FHIR JSON and XML bodies, the SNOMED CT implicit?fhir_vs=and?fhir_cm=forms, and the RF2 file names, effective times, and identifiers) is fed arbitrary bytes bycargo-fuzz, weekly in CI and by hand for longer runs. The seeds are committed; a crash uploads the input that reproduces it.
Fixed
- The terminology capabilities no longer claim SNOMED CT post-coordination.
CodeSystem.compositionalis "The code system defines a compositional (post-coordination) grammar" andTerminologyCapabilities.codeSystem.version.compositionalis "If the compositional grammar defined by the code system is supported", two different statements that one flag answered, so a client was told it could send expressions and then got "no such concept" for every one. A provider now declares the grammar and this server's support for it separately: SNOMED CT defines the grammar and this server evaluates none, UCUM, BCP 13, BCP 47, and the ICD-11 linearizations parse the grammars they declare, and aCodeSystemresource that declares a grammar is served without one. A post-coordinated code is refused as a grammar this server does not evaluate (not-supported) rather than as a concept the edition lacks. - A code system the deployment loaded reads at its own id and is found by a search.
GET CodeSystem/{id}on the id the server prints at startup was a404andGET CodeSystem?url=…answeredtotal0, while the capability statement declaredreadandsearch-typeand the configuration page documented the id, so a documented instance path addressed nothing. Every loaded version now answers both, under every version prefix: a system held behind an index answers its definition withcontent = not-present, and one loaded as aCodeSystemresource answers with thecontentit declared. A supplement stays applied to the system it supplements and is not served as an instance. CodeSystem/{id}/$lookupanswers under/r5and/r6, whoseOperationDefinitiondeclares the operation at the instance level. The engine refused the instance level on every version; the level is now the served version's own declaration, so/r4and/r4b, which declare the type level only, still offer no such route.- A SNOMED CT implicit value set or concept map based on a non-default edition answers when several editions are loaded. The FHIR SNOMED CT page lets any edition version stand as the base URL, and the edition in the base decides the membership, but the server only asked the default version of the system, so every
?fhir_vsform and?fhir_cm=on another loaded edition was refused as a malformed URI. The registry now asks every loaded version, default first, so the barehttp://snomed.info/sctbase still answers from the default edition. A base naming an edition version no loaded version serves is anot-foundon that version rather thanvs-invalid. $translateover a SNOMED CT map reference set returns a targetCodingcarrying both asystemand acode. R4B needsConceptMap.group.target, the "absolute URI that identifies the target system that the concepts will be mapped to", unless the target value set names a single system or every target equivalence isunmatched, and an implicit map states neither. RF2 records the scheme nowhere on a member row, so the reference set names it: the ICD-10 extended map (447562003), the ICD-9-CM equivalence map (447563008), the ICD-O map (446608001), the CTV3 map (900000000000497000), and the US Edition's ICD-10-CM map (6011000124106) each state the URI FHIR publishes for their scheme, on the servedConceptMapand on every match. A map reference set outside that table answersnot-supportedinstead of a code with no system. An association reference set names SNOMED CT on both sides, whichever reference set it is.- The LOINC and RxNorm builds name a property key they never registered instead of reporting "too many concepts": a defect in the build no longer surfaces as a capacity error. The RxNorm relationship-type lookup is split the same way, and
TooManykeeps its own meaning. - An expression constraint of deeply nested brackets no longer takes the server down. The ECL parser descends with the grammar, so an unbounded nesting exhausted the stack and aborted the process, which no
OperationOutcomesurvives; the parser now refuses pastsct_ecl::NESTING_LIMIT(64) and the server answers the refusal as anOperationOutcome. Found by theecl_parsefuzz target.
Changed
- A flat
$expandanswers in the order the compose selected: the includes in order, each include's named concepts as it named them, each filter's concepts in the code system's own order, and the first occurrence winning where includes overlap.countandoffsetpage over that order. No FHIR version fixes the order of an expansion, andValueSet.compose.include.conceptsays an expansion typically follows the compose; a compose of one filter still pages off the selection bitmap without reading a concept. The ICD-11 postcoordination axis now answers in the WHO's own scale order, which the terminology ecosystem suite'sicd-11/expand-pcs-countandicd-11/expand-pcs-offsetcases assert together.
0.0.11
Added
- The build layers derivative reference set packages onto the edition they depend on:
ferroterm-build --rf2 <edition> --rf2-refset <package>takes the flag repeatably, and each package's concepts, descriptions, and language reference set members join the edition's while its reference sets serve as implicit value sets over it. Every package's module dependency is checked first, so an edition older than the date a package was authored against is refused, naming the module and both dates, and so is a package naming a module the edition does not contain. The manifest records what was layered. The SNOMED CT ICNP Nursing Practice package is the shape this serves. ConceptMap/$closuremaintains a named transitive closure table for a client, over any code system with subsumption:namealone creates or empties the table at version0,namewith concepts answers the relationships the client did not have at a new version, andnamewith a version replays everything sent after it. The tables live in the databaseFERROTERM_RESOURCESnames and outlive a restart. Naming a table the server was not asked to create is a404, and a table whose code systems changed under it answers422withmust be reinitialized. The relationship is read from target to source, and each version states it in its own vocabulary. The R6 ballot ships noConceptMap-closuredefinition, so/r6offers no$closureand declares none.- The batch interaction:
POST [base]with aBundleof typebatchruns every entry and answers abatch-responsewith one entry per request, in order, on every served version. AGETentry carries the operation's inputs in the query ofrequest.urland aPOSTentry in aParametersresource; every terminology operation is reachable, at the type level and at the instance level. The entries are independent: one that fails answers theOperationOutcomethe same request would have answered on its own, the rest still answer, and the batch answers200. AtransactionBundle is refused withnot-supported, and the capability statement declares thebatchsystem interaction. - Persisted
CodeSystem,ValueSet, andConceptMapresources: a deployment that names a database inFERROTERM_RESOURCESaccepts create, update, read, version read, search byurlandversion, and delete on every served version, with the FHIR status codes and theETagandLast-Modifiedheaders.meta.versionIdstarts at1and rises with every write,If-Matchmakes a write conditional, a deleted resource reads as410 Goneand keeps its history. Every operation sees a persisted resource exactly as it sees one loaded fromFERROTERM_CODESYSTEMS, on every version, and the resources are served again after a restart. A deployment that names no database refuses every write with a422and declares no write interaction in its capability statement. - SNOMED CT implicit concept maps:
ConceptMap/$translateanswersurl=http://snomed.info/sct?fhir_cm=[sctid]over the reference sets of the loaded edition, on the bare system URI or on the edition or version URI. The four association reference sets the FHIR SNOMED CT page lists (POSSIBLY EQUIVALENT TO,REPLACED BY,SAME AS,ALTERNATIVE) each carry the relationship that page gives them, and the map follows the page's template. A map reference set answers frommapTarget, with the complex and extended columns (mapGroup,mapPriority,mapRule,mapAdvice,correlationId,mapCategoryId) asproductparts; no RF2 file records which system amapTargetbelongs to, so the group names no target system. A$translatethat names no map and finds none falls back to the historical associations of an inactive concept, so a retired code answers with itsSAME ASandREPLACED BYsuccessors. Where the specifications are silent, the choice is marked in the code as our own design. $expandcarries the code system's hierarchy: a compose of one include that selects a whole system or anis-asubtree nests itsexpansion.contains, withtotalcounting every concept andcountandoffsetpaging over the pre-order flattening.excludeNestedkeeps it flat, and so does a textfilterover a whole system, whose matches have no root to hang from.
Changed
- The four loader pipelines SonarQube reports as most complex are split along their phases, with no change in behaviour: the LOINC and RxNorm builds and the DHD thesaurus and G-Standaard readers each drop from roughly 250 lines to under 60, and the four
too_many_linessuppressions they carried are gone. The designation and property-key accumulators become small owned structs rather than closures over the enclosing scope.