Configure terminology routing
Coded ports ({ kind: "coded", valueSet }) are validated against a
TerminologyPort. The instance's terminology: map routes each code system to
the resolver that owns it — a plugin-contributed adapter (e.g. SNOMED CT via
Hermes) — and lets every other system fall through to the built-in registry.
This recipe adds one routing entry and verifies it resolves.
For what a coded type is and how value-sets fit the type system, see The type system. For adding a value-set or plugging a new resolver plugin, see Add vocabulary and types.
1. Identify the adapter to route to
Terminology resolvers are contributed by plugins as terminology adapters —
the same mechanism as any other port adapter
(libs/shared/src/plugin/manifest.ts, AdapterContribSchema). The bundled
@posos/snomed plugin is the concrete example
(external-plugins/snomed/plugin.yaml):
contributes:
adapters:
- port: terminology
name: hermes
module: ./adapters/hermes.ts
systems: [snomed-ct] # informational only — not used for routing
The name (hermes) is what you reference from the instance manifest. systems
documents which code systems the adapter is meant for, but it does not drive
routing by itself — routing is entirely decided by the instance's terminology:
keys (next step). Make sure the plugin is loaded first: see
Declare and install plugins.
2. Add a routing entry to the instance manifest
In instance.yaml (or the file pointed to by INSTANCE_CONFIG), add one entry
per code system under terminology:, keyed by the code system identifier
(the value-set id, e.g. snomed-ct) and pointing at an adapter name plus
its config:
terminology:
# routing by code system; systems not listed fall back to internal resolution.
snomed-ct: { adapter: hermes, config: { baseUrl: http://localhost:8081 } }
config is passed verbatim to the adapter's factory
(apps/api/src/instance.ts, buildTerminology):
for (const [system, binding] of Object.entries(instance.terminology)) {
const ad = findAdapter(plugins, "terminology", binding.adapter);
if (!ad) { log.warn({ system, adapter: binding.adapter }, "résolveur terminology introuvable"); continue; }
providers[system] = ad.factory(binding.config) as TerminologyPort;
}
For the Hermes adapter, config.baseUrl is read by
external-plugins/snomed/adapters/hermes.ts (falling back to the
HERMES_BASE_URL env var, then http://localhost:8081 if neither is set). If
binding.adapter doesn't match any loaded adapter's name, the system is
logged as a warning at boot and silently gets no dedicated resolver — it falls
through to the same internal default as an unlisted system (see next step).
3. Leave every other system unrouted
You do not need an entry for systems that should resolve internally. The
resulting RoutingTerminology (libs/shared/src/domain/terminology.ts) tries
providers[system] first and falls back to InternalTerminology — the
in-repo value-set registry (libs/shared/src/domain/value-sets.ts) — for
anything not in the map:
private route(valueSet: string): TerminologyPort {
return this.providers[valueSet] ?? this.fallback; // fallback = new InternalTerminology()
}
This fallback is fixed to the internal registry — there is no manifest option
to point unrouted systems at a generic external server instead. If you need a
system resolved externally, contribute (or select) a terminology adapter for
it explicitly, as in step 1.
Be aware of one asymmetry: a value-set contributed with external: true (like
snomed-ct itself — its codes aren't enumerated in-repo) has no concepts for
InternalTerminology to check against, so its internal validateCode fails
open (accepts any code) rather than rejecting it
(libs/shared/src/domain/terminology.ts, InternalTerminology.validateCode).
In practice this means: route every external value-set you actually want
validated, and treat "unrouted + external" as unchecked, not as "denied".
4. Verify the routing
Run the instance-selection test, which asserts both branches — a routed system
resolving through the adapter, and an unlisted system falling back internally
(apps/api/tests/test-instance.ts):
cd apps/api
pnpm run test:instance
It exercises:
const okSnomed = await ports.terminology!.validateCode("snomed-ct", "24700007");
// → true, routed to Hermes
const okGender = await ports.terminology!.validateCode("administrative-gender", "male");
// → true, unlisted system, resolved by the internal fallback
test:instance passes even without Hermes reachable — the adapter fails open
and logs a warning (hermes.ts: "serveur injoignable … validation SNOMED en
mode fail-open"). To exercise a real Hermes round-trip (start it first on
http://localhost:8081), run the dedicated end-to-end test instead:
pnpm run test:snomed
5. The deployed instance
Staging runs Hermes in-cluster as applications/apps/meridian-hermes
(posos-tech/vanilla-argonaute) — a StatefulSet with a PVC, reachable on
ClusterIP only, and the routing entry from step 2 is already in the
meridian-instance ConfigMap pointing at
http://hermes.ps-staging-apps.svc.cluster.local:8080. Two things differ from a
Hermes you run locally:
The database is built on first boot, by two initContainers, from the SNOMED CT édition nationale française RF2 held in a private GCS bucket. That edition is a full edition, not a language extension — it carries the international content it depends on, so it is a single import. The ANS download is authenticated (SMT account + French NRC affiliate number), so the zip is uploaded by hand, once per annual (June) release.
French display is selected by the adapter, not by the server. Hermes resolves locales through a hardcoded table in which
fr-frmeans the international French language reference set,722131000. The ANS national edition ships its own,10031000315102, which that table does not know — so--locale frmakes the server refuse to start, and Hermes's documented escape hatch (a BCP 47 private-use tag,fr-x-10031000315102) fails too, because the match is then filtered against the recognised locales only. The server therefore runs onen-US, and the routing entry carrieslanguageRefsetId: 10031000315102so the adapter picks the term preferred in the French refset off/extended.The translation is delivered incrementally, so
display()returns French where translated and falls back to the server's preferred term elsewhere.--bind-address 0.0.0.0is required. Hermes binds to loopback by default, which is invisible to both the Service and the kubelet's readiness probe.
Related
- Add vocabulary and types — add a value-set or a new terminology adapter.
- Declare and install plugins — get the resolver's plugin loaded in the first place.
- Instance manifest — full
terminology:andports:schema. - The type system — coded types and value-sets.
- Bundled plugins — what
@posos/snomedand the other core plugins contribute.