Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ name: CI

on:
push:
branches: [main]
branches:
- main
- 'wip/**'
pull_request:

concurrency:
Expand Down Expand Up @@ -211,8 +213,8 @@ jobs:
- uses: actions/cache/restore@v5
id: cache
with:
path: .hades/loinc-2.81.db
key: loinc-2.81-v3
path: .hades/loinc-2.82.db
key: loinc-2.82-native-v1

- name: Provision pinned LOINC DB
id: provision
Expand All @@ -226,28 +228,28 @@ jobs:
fi
mkdir -p .hades
extract_dir=$(mktemp -d)
gh release download 2.81 \
gh release download 2.82 \
--repo wardle/loinc-releases \
--pattern 'Loinc_2.81.zip' \
--pattern 'Loinc_2.82.zip' \
--output "$extract_dir/loinc.zip"
# Extract into a directory whose name encodes the version so
# the LOINC loader's infer-version regex (Loinc[_-]?\d+...)
# matches whether the zip unpacks to LoincTableCore/... at the
# top level or nests inside its own Loinc_2.81/ folder.
unzip -q "$extract_dir/loinc.zip" -d "$extract_dir/Loinc_2.81"
release_dir=$(dirname "$(find "$extract_dir/Loinc_2.81" -maxdepth 3 -type d -name LoincTableCore | head -n 1)")
if [ -z "$release_dir" ] || [ ! -f "$release_dir/LoincTableCore/LoincTableCore.csv" ]; then
echo "::error::Could not locate LoincTableCore/LoincTableCore.csv inside the LOINC zip."
# local path is clear in logs whether the zip unpacks files at
# the top level or nests inside its own Loinc_2.82/ folder.
unzip -q "$extract_dir/loinc.zip" -d "$extract_dir/Loinc_2.82"
loinc_table_dir=$(find "$extract_dir/Loinc_2.82" -maxdepth 4 -type d -name LoincTable | head -n 1)
release_dir=$(dirname "$loinc_table_dir")
if [ -z "$loinc_table_dir" ] || [ ! -f "$release_dir/LoincTable/Loinc.csv" ]; then
echo "::error::Could not locate LoincTable/Loinc.csv inside the LOINC zip."
exit 1
fi
clojure -M:run import compact .hades/loinc-2.81.db "$release_dir"
clojure -M:run import compact .hades/loinc-2.82.db "$release_dir"
rm -rf "$extract_dir"

- uses: actions/cache/save@v5
if: always() && steps.provision.outcome == 'success'
with:
path: .hades/loinc-2.81.db
key: loinc-2.81-v3
path: .hades/loinc-2.82.db
key: loinc-2.82-native-v1

# The install CLI downloads + extracts + imports in one shot:
# --cache-dir lands extracted JSON at <cache>/<id>-<version>/package/
Expand Down Expand Up @@ -362,8 +364,8 @@ jobs:

- uses: actions/cache/restore@v5
with:
path: .hades/loinc-2.81.db
key: loinc-2.81-v3
path: .hades/loinc-2.82.db
key: loinc-2.82-native-v1
fail-on-cache-miss: true

- uses: actions/cache/restore@v5
Expand Down Expand Up @@ -426,8 +428,8 @@ jobs:

- uses: actions/cache/restore@v5
with:
path: .hades/loinc-2.81.db
key: loinc-2.81-v3
path: .hades/loinc-2.82.db
key: loinc-2.82-native-v1
fail-on-cache-miss: true

# Cache the tx-ecosystem clone keyed on the pinned commit. Bump
Expand Down Expand Up @@ -498,8 +500,8 @@ jobs:

- uses: actions/cache/restore@v5
with:
path: .hades/loinc-2.81.db
key: loinc-2.81-v3
path: .hades/loinc-2.82.db
key: loinc-2.82-native-v1
fail-on-cache-miss: true

- uses: actions/cache/restore@v5
Expand All @@ -526,7 +528,7 @@ jobs:
run: |
clojure -M:run serve \
.hades/snomed-intl-20250201.db \
.hades/loinc-2.81.db \
.hades/loinc-2.82.db \
.hades/fhir-smoke.db \
--port 8080 &
echo $! > hades.pid
Expand Down
82 changes: 44 additions & 38 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,42 +127,47 @@ Source layout:
- `src/com/eldrix/hades/core.clj` — public library API: service lifecycle
and the FHIR terminology operations as plain Clojure functions. Read the
ns docstring for the current surface.
- `src/com/eldrix/hades/impl/**` — library internals. Subject to breaking
changes; nothing outside the project should depend on these namespaces.
- `src/com/eldrix/hades/cmd.clj` + `impl/cli.clj` — CLI application
(entry point + option parsing). Not part of the library surface.

The internal layering under `impl/` (read each file for the current
public surface — names drift, the layering doesn't):
- `src/com/eldrix/hades/composite.clj` — TerminologyService that dispatches
across registered providers; what `core` returns.
- `src/com/eldrix/hades/protocols/**` — `CodeSystem`, `ValueSet`,
`ConceptMap` interfaces plus result/input specs. The contract between
composite and providers.
- `src/com/eldrix/hades/providers/**` — one subdirectory per provider;
`providers/common/` holds shared provider helpers.
- `src/com/eldrix/hades/impl/**` — transport / wire layer (HTTP, MCP,
CLI parsing, wire shaping, source walking). Subject to breaking change.
- `src/com/eldrix/hades/cmd.clj` + `impl/cli.clj` — CLI application.

Layering (read each file for the current public surface — names drift,
the layering doesn't):

```
impl/http.clj Pedestal HTTP layer — routes, interceptors,
Parameters parsing, content negotiation
impl/http.clj Pedestal HTTP — routes, interceptors,
Parameters parsing, content negotiation
impl/mcp/{server,tools}.clj MCP stdio transport + tool definitions
│
impl/wire.clj, impl/metadata.clj Pure FHIR JSON map builders (string-keyed)
│
impl/wire.clj Pure FHIR JSON map builders (string-keyed)
impl/metadata.clj
composite.clj TerminologyService — dispatch by URL/version,
cross-provider concerns (status warnings,
supplements check, CodeableConcept aggregation)
│
impl/composite.clj TerminologyService — dispatch by URL/version,
cross-provider concerns (status warnings,
supplements check, CodeableConcept aggregation)
protocols.clj, protocols/{input,result} Abstract interfaces + result/input specs
│
impl/protocols/ Abstract interfaces and result/input specs
(CodeSystem, ValueSet, ConceptMap)
providers/snomed/{provider,batch, SNOMED CT via Hermes
expansion}.clj
providers/in_memory/{provider,index}.clj In-memory providers + indexer
providers/ftrm/{provider,db,index}.clj FTRM (SQLite-backed) provider + container + indexer
providers/loinc/{loader,store,index, Native LOINC provider + container + indexer
import,provider,model}
providers/common/ Shared provider helpers: compose, vs-validate,
display, canonical, issues, property-filter,
fhir-extract, supplement, fhir-loader
│
impl/snomed.clj SNOMED CT via Hermes
impl/in_memory.clj In-memory providers
impl/sqlite/provider.clj SQLite-backed catalogue providers
impl/compose.clj ValueSet compose engine (include/exclude, filter, etc.)

impl/loaders/fhir.clj FHIR JSON → fhir-data
impl/loaders/loinc.clj LOINC release dir → fhir-data
impl/index/memory.clj fhir-data → in-memory providers
impl/index/sqlite.clj fhir-data → SQLite container
impl/load.clj Convenience constructors over the load pipeline
impl/sqlite/db.clj SQLite open/create/schema/pragmas

cmd.clj CLI entry point (-main)
impl/cli.clj CLI option parsing
impl/sources.clj, impl/paths.clj Walk a path, recognise terminology files,
impl/load.clj, impl/fhir_package.clj open them as provider bundles

cmd.clj, impl/cli.clj CLI entry point + option parsing
```

### Layering rules (strict)
Expand All @@ -179,11 +184,12 @@ impl/cli.clj CLI option parsing

### Layer responsibilities (strict)

- **Protocol impls** (snomed.clj, in_memory.clj, sqlite/provider.clj) know
their domain. They return **complete, self-describing results** that match
the result specs under `impl/protocols/` — read those for the current
contract. No downstream layer should need to patch, enrich, or re-derive
fields.
- **Provider impls** (`providers/snomed/provider.clj`,
`providers/in_memory/provider.clj`, `providers/ftrm/provider.clj`, …)
know their domain. They return **complete, self-describing results**
that match the result specs under `protocols/` — read those for the
current contract. No downstream layer should need to patch, enrich,
or re-derive fields.
- **Composite** dispatches to the right impl by URL/version and handles
version resolution (the version-resolution flags carried in the operation
params). It does **not** patch results, add issues retroactively, look up
Expand Down Expand Up @@ -262,7 +268,7 @@ handler's params; grep `impl/http.clj` for the current names.
- Use `clojure.spec.alpha` for all public API boundaries: protocol parameters,
operation inputs, constructor arguments, **and protocol return values**.
- The canonical data shapes for protocol parameters and return values live
under `impl/protocols/` — read those files; they are the contracts between
under `protocols/` — read those files; they are the contracts between
layers, and every protocol impl must return data conforming to them.
- Namespace specs under the relevant module.
- Write `s/fdef` for public functions with non-trivial parameter contracts.
Expand Down Expand Up @@ -366,7 +372,7 @@ place and flows data correctly.
them and what shape do they expect? Draw the path: http → composite → impl
→ composite → http → wire. If data needs to flow backwards (handler reaching
back into impl metadata), the return value is incomplete.
4. **Check the spec.** Does a spec exist under `impl/protocols/` for the
4. **Check the spec.** Does a spec exist under `protocols/` for the
return value this change produces? If not, define it before writing the
implementation. The spec is the contract — write the contract first.
5. **Check for secret channels.** Does this change require smuggling data through
Expand Down Expand Up @@ -417,7 +423,7 @@ Run after every task. All items must pass.
methods to recover data that should be in the result, no ad-hoc keys
smuggled through the request context.
7. **Protocol impls return complete results.** If you changed a protocol impl,
its return value must conform to the result spec under `impl/protocols/`.
its return value must conform to the result spec under `protocols/`.
No downstream patching needed.
8. **Composite is not patching domain results.** If you added logic to the
composite that modifies a result after the protocol call (adding issues,
Expand Down
6 changes: 5 additions & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,9 @@

:lint/kondo
{:extra-deps {clj-kondo/clj-kondo {:mvn/version "RELEASE"}}
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]}}}
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]}

:lint/splint
{:extra-deps {io.github.noahtheduke/splint {:mvn/version "1.24.0"}}
:main-opts ["-m" "noahtheduke.splint" "src"]}}}

22 changes: 11 additions & 11 deletions doc/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ exist locally. Fixture paths are declared in
|----------------------------------|-----------------------------------|------------------------------------------|------------------------------------------|
| SNOMED CT International | `.hades/snomed-intl-20250201.db` | `ihtsdo.mlds/167@2025-02-01` | MLDS |
| SNOMED CT UK monolith | `.hades/snomed-uk-monolith.db` | `uk.nhs/sct-monolith` (latest) | TRUD |
| LOINC | `.hades/loinc-2.81.db` | LOINC release `2.81` | loinc.org (free account) |
| LOINC | `.hades/loinc-2.82.db` | LOINC release `2.82` | loinc.org (free account) |
| FHIR packages (SQLite container) | `.hades/fhir-smoke.db` | `hl7.fhir.r4.core@4.0.1` + 5 others | packages.fhir.org |
| FHIR packages (unpacked cache) | `.hades/fhir-packages/` | same as above | packages.fhir.org |
| tx-ecosystem (conformance) | `.hades/tx-ecosystem/` | rev pinned in `conformance_test.clj` | `HL7/fhir-tx-ecosystem-ig` (auto-cloned) |
Expand Down Expand Up @@ -65,16 +65,16 @@ rm -f .hades/trud-api-key.txt

LOINC is licensed and not redistributable, so you download it yourself:
sign in at [loinc.org/downloads](https://loinc.org/downloads/) (free
account), grab the **LOINC Table File (CSV)** archive for release 2.81,
and unzip it. Then point `import` at the unzipped directory:
account), grab the **LOINC Table File (CSV)** archive for release 2.82,
and unzip it:

```bash
unzip /path/to/Loinc_2.81.zip -d /tmp/Loinc_2.81
clj -M:run import compact .hades/loinc-2.81.db /tmp/Loinc_2.81
unzip /path/to/Loinc_2.82.zip -d /tmp/Loinc_2.82
```

`import` auto-detects the LOINC release layout (it looks for
`LoincTableCore/LoincTableCore.csv`).
LOINC release detection is based on the native loader's required
`LoincTable/Loinc.csv`. Native LOINC import writes a LOINC-specific
SQLite store from the release CSVs.

### FHIR packages

Expand Down Expand Up @@ -122,7 +122,7 @@ asserts the in-memory and SQLite/FTRM providers produce **the same**
result for the same fhir-data — including human-readable text. Both
`:message` and issue `:text` are part of the comparison contract:
divergence means the provider must converge on a shared helper in
`impl/issues.clj`, not that the test's normaliser should look the
`providers/common/issues.clj`, not that the test's normaliser should look the
other way. The synthetic fixture is small (a handful of toy
CodeSystems / ValueSets / a ConceptMap), so failures are fast to
triage.
Expand Down Expand Up @@ -232,7 +232,7 @@ cd ~/Dev/hades
RUN_ID="$(date -u +%Y-%m-%dT%H%M)-2.0.$(git rev-list --count HEAD)-$(git rev-parse --short HEAD)$(git diff-index --quiet HEAD -- src test deps.edn build.clj || echo -dirty)"
clj -M:run serve \
.hades/snomed-uk-monolith.db \
.hades/loinc-2.81.db \
.hades/loinc-2.82.db \
.hades/fhir-smoke.db \
--port 8080 > /tmp/hades.log 2>&1 &
HADES_PID=$!
Expand Down Expand Up @@ -267,7 +267,7 @@ cd ~/Dev/hades
RUN_ID="$(date -u +%Y-%m-%dT%H%M)-2.0.$(git rev-list --count HEAD)-$(git rev-parse --short HEAD)$(git diff-index --quiet HEAD -- src test deps.edn build.clj || echo -dirty)"
clj -M:run serve \
.hades/snomed-uk-monolith.db \
.hades/loinc-2.81.db \
.hades/loinc-2.82.db \
.hades/fhir-smoke.db \
--port 8080 > /tmp/hades.log 2>&1 &
HADES_PID=$!
Expand Down Expand Up @@ -341,7 +341,7 @@ RUN_ID="$(date -u +%Y-%m-%dT%H%M)-2.0.$(git rev-list --count HEAD)-$(git rev-par
clj -T:build uber
java -Xmx6g -jar target/hades.jar serve \
.hades/snomed-uk-monolith.db \
.hades/loinc-2.81.db \
.hades/loinc-2.82.db \
.hades/fhir-smoke.db \
--port 8080 > /tmp/hades.log 2>&1 &
HADES_PID=$!
Expand Down
2 changes: 1 addition & 1 deletion doc/ftrm.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This document is the contract.
| First published | 2026-05 |
| Stability | Stable. Breaking changes will bump the schema version and ship migration tooling. |
| Reference implementation | Hades — <https://github.com/wardle/hades> |
| Reference DDL | [`resources/com/eldrix/hades/sqlite/schema-v1.sql`](../resources/com/eldrix/hades/sqlite/schema-v1.sql) |
| Reference DDL | [`resources/com/eldrix/hades/providers/ftrm/schema-v1.sql`](../resources/com/eldrix/hades/providers/ftrm/schema-v1.sql) |
| Licence | The schema, identifiers and prose of this specification are licensed under CC0. Implementations may use any licence. |

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**,
Expand Down
Loading