Perfuse

Documentation

What was tested, what it found, and what was never run

By BioDream Developer, BioDream LLC · about 105 minutes to read

What has been verified, and what that found

Most integration software is tested against its own idea of what the other side does. That is how a product acquires a thousand passing tests and a feature that has never once worked against the real thing — and it is not a hypothetical failure, it happened in this repository and is recorded below.

So the parts of Perfuse that talk to other people's software are checked against that software: a real Mirth Connect, a real Keycloak, a real Microsoft Entra tenant, a real HAPI FHIR server, a real PACS, a real PostgreSQL, a real OpenSSH server, real OpenSSL. This document says what was verified, what version it was verified against, and what the verification found — because a claim of verification with no record behind it is exactly the kind of assertion this project exists to distrust.

The Inferno runs (CRD, DTR, PAS, SMART App Launch and US Core 7) can be repeated from this repository: conformance/inferno/run.sh <suite> checks out the kit at the version recorded here, starts Perfuse with the same flags and data, and prints the results. See conformance/inferno.

Every finding here was produced by running the software against another implementation. None of them were found by reading code, and several had a substantial body of passing tests sitting on top of them.


SAML, against Keycloak 26

What was found: no identity provider could ever have signed anybody in, and about a thousand lines of tests said otherwise.

A real browser sign-in was driven end to end against Keycloak 26. The first genuine assertion failed with a digest mismatch, and the cause was in the canonicalisation: the canonical form that a signature covers was being computed with namespace prefixes dropped. That produced a different document from the one Keycloak had signed, so the signature could never have matched. Verification could not have succeeded for any provider, ever.

Keycloak writes the assertion as saml:Assertion and declares xmlns:saml on the document root. The canonical form came out unprefixed, and every test in the suite agreed with it, because every test had been written against the same incorrect canonicaliser. The tests were self-consistent and collectively worthless — they proved the code agreed with itself.

The fixture in internal/saml/testdata is a document a real Keycloak published. It is kept because a document written by hand could never have caught this.

SAML, against Microsoft Entra ID

What was found: five behaviours no reading of the specification predicts, and two defects in the code surrounding the protocol.

A real interactive sign-in was completed against a live Entra tenant, with a real Microsoft account. The security-critical path — signature, request binding, conditions, audience — was correct the first time it saw a real Entra document. Everything that was wrong was around it.

Five behaviours that documentation and examples do not lead you to expect:

  1. No email claim at all. Entra sends display name, identity provider, object identifier, tenant ID

and name. No email address.

  1. The NameID is an opaque persistent identifier, not an address and not readable.
  2. No groups claim is sent unless the application is explicitly configured to emit one, so a role

mapping written against groups refuses every sign-in. This happened on the first attempt.

  1. When groups are emitted they are object GUIDs, not names, unless the tenant synchronises from

on-premises Active Directory.

  1. Claim URIs use schemas.microsoft.com/identity where published examples show

schemas.xmlsoap.org. Perfuse works here only because attribute lookup compares by URI suffix. A stricter comparison would look more correct and would lose the display name for every Microsoft tenant. That now has a test.

Two defects, both found by looking at the users screen after signing in rather than by reading code:

  • The account was created under Entra's opaque NameID. The readable-name logic derived a username

from an email claim, and Entra sends none, so it fell through to an unreadable identifier — which would then have appeared beside every channel that account changed. It now reads the user principal name from the name claim. Break-verified: removing the lookup reproduces the unreadable name exactly.

  • A SAML account was recorded as OIDC. No AuthSAML constant existed, and the store derived the

authentication source from the issuer, which cannot work when a SAML issuer and an OIDC issuer are both HTTPS URLs. A site running both could not have told its accounts apart, which is the question that matters when somebody leaves.

The lasting artefact is the fixture pair. Entra writes <Assertion> and <Signature> with no namespace prefix; Keycloak writes <saml:Assertion> and <dsig:Signature>. Those are the two opposite cases in canonicalisation, and the defect described in the previous section was in prefix handling — so one fixture covered only one side of it. A test asserts that each document continues to cover its own case, because a replacement capture that happened to be prefixed would quietly halve the coverage without failing anything.

Mirth Connect 4.5.2 — reading its channels

What was found: the repository's only Mirth fixture was written by hand, and a real Mirth refused to load it.

The fixture declared what it was in its own header — written by hand, not derived from any deployment. Tested against Mirth 4.5.2 in a container, Mirth accepted the upload and stored the channel as invalid.

Mirth now writes the fixtures. A script has Mirth's own serialiser emit its defaults, and those documents are committed. A document this project wrote could never have been evidence about what Mirth accepts.

Mirth Connect 4.5.2 — writing its channels

What was found: the exporter had nine passing tests, and Mirth discarded everything it produced.

Every one of those nine tests was a round trip through the same package: write XML, read it back with our own parser, compare. The exporter's own documentation admitted the doubt in writing — that Mirth "may or may not accept" the result — and nothing had ever checked.

The reason it stayed hidden is worth stating on its own: Mirth does not refuse a channel it cannot assemble. It stores one, replaces the description with a sentence saying the channel is invalid, drops the destinations, and returns success. Acceptance is not verification.

Four faults, each break-verified against the running server:

  1. The channel carried an enabled element. Mirth's model has no setter for it, so its deserialiser

discarded the entire channel. This had been documented in a fixture comment since August and never reached the code it described.

  1. Destinations were written as a bare connector element, with the version only on the source — and

an existing test asserted that bare form, holding the defect in place.

  1. Nested property elements lost their attributes, because the importer flattened them to dotted

paths which cannot carry a class attribute or per-element versions. Fixed by retaining the parsed subtree and writing it back verbatim.

  1. Resource identifiers were written as a single string where Mirth writes a key/value pair, and MLLP

framing properties were missing entirely.

A further gap was found by deliberately breaking the exporter: Mirth accepts an unknown transport name without complaint. A source set to a transport that does not exist still stored as valid, because the deserialiser resolves the properties class and takes the transport name on trust. The mistake would surface only on deploy. Transport names are now asserted explicitly.

Ten transport pairs are verified by importing into a running Mirth server: MLLP, TCP, HTTP, file, database, DICOM in both directions, SMTP, SOAP and JavaScript destinations.

What does not convert is reported by name and never approximated. Filters, transformations, scripts, contracts, shadow comparisons, attachment extraction and mapping tables have nowhere to live in Mirth's format, and each is listed before the file is offered. A destination type that cannot be honoured is refused by name rather than replaced with something close.

What was found: four defects in the importer, and three whole document kinds it could not read at all.

Mirth Connect went commercial-only at 4.6 (March 2025). A site leaving it starts from Mirth 4.5.2, the Open Integration Engine (the Eclipse-hosted fork) or BridgeLink (Innovar's fork), so the importer and exporter were run against all of them: Mirth 4.5.2 (nextgenhealthcare/connect), OIE 4.5.2 (openintegrationengine/engine), OIE 4.6.0 (built from the project's signed release tarball, checksum verified, since it publishes no 4.6 image) and BridgeLink 26.9.0 (innovarhealthcare/bridgelink).

The corpus is written by the engines, not here. scripts/mirth-engine-corpus.sh compiles BuildCorpus.java against each engine's own jars and has its own ObjectXMLSerializer write three channels (MLLP with a mapper, a script calling a code template library, a rule-builder filter and two destinations; HTTP into a database; a file poll out over MLLP), a code template library and a channel group. It loads them into the running engine, checks none was stored as invalid, and downloads the engine's own server backup. The four sets are committed under internal/mirth/testdata/engines/ and checked on every build without any engine running.

Found:

  • Server backups, channel group exports and code template exports could not be read — the importer accepted only <channel> and a list of them. A channel calling a site library imported cleanly and would have failed on its first message with an undefined function. All three are now read, and each library becomes lib/<name>.js, included by exactly the channels Mirth had it enabled for.
  • HTTP Listener and File Reader sources were refused as "only MLLP listeners are supported", long after Perfuse had both. JavaScript, DICOM, Web Service and raw TCP sources, and SMTP, JavaScript, DICOM, Web Service, raw TCP and Channel Writer destinations, translate now too. perfuse explain had its own stale table calling the Database and DICOM connectors unsupported; a test now holds it to agree with translate on every engine's channels.
  • An HTTP Sender's URL was read from url, a field none of the four engines writes; they all use host.
  • Mirth's socket bufferSize (65536) was translated as max_message_size, so every message over 64 KB would have been refused.
  • A Mapper step's variable became a JavaScript global. Mirth puts it into channelMap, where $('mrn') reads it; the translated script ran the library on an empty string. Found only by running the translated channel: TestEveryEnginesADTChannelRunsWithItsLibrary sends an ADT and an ORU to each engine's translated channel and checks the record number was padded by the library and the ORU filtered.

The other direction: every live test of Perfuse's Mirth export now runs once per engine (internal/mirth/mirthlive). Channels Perfuse exports, ten transport pairs among them, load in all four without being marked invalid. BridgeLink stamps its own version (26.9.0) on everything it writes; nothing in Perfuse depended on the version being 4.x.

Open Integration Engine 4.7.0 (23 October 2026). Built from the project's release tarball (checksum verified) and run the same way: it wrote its own corpus and server backup (internal/mirth/testdata/engines/oie-4.7.0), which read, translate, load and run like the others, and every Perfuse export loads in it without being marked invalid. What changed in its documents: the HTTP Sender writes overrideLocalBinding and localAddress, and the HTTP Listener requestHeaderSize. Perfuse's HTTP destination has no local binding, so a sender bound to a chosen address is now a warning; its listener accepts headers up to 1 MB, so only a larger limit is. 4.7.0 also gives admin a random password on first start (written once to its log) where earlier engines used admin; the test container sets server.initialadminpassword.

What this does not show: channels authored by hand in each Administrator, which may use settings the corpus does not; and plugins beyond those the images ship.

AMQP 1.0, against RabbitMQ 4 and ActiveMQ; Azure Blob, against Azurite

What was found: two protocol defects that the codec's own round-trip tests could not have shown.

The AMQP 1.0 client (internal/amqp) sends, receives and settles against RabbitMQ 4 and ActiveMQ Classic 6.2: three messages, the third 250 KB and so split across frames; the first released and redelivered; the rest accepted once. A channel reading one queue and writing another, on each broker, delivered the message with its control id and MSH-9 as the AMQP message-id and subject, and left the source queue empty.

  • Links were matched by our handle number rather than the broker's. Each side numbers its own handles, and RabbitMQ's happened to

agree with ours, so everything passed there; ActiveMQ numbered differently and the second link never saw its attach.

  • A split transfer put the more flag in the wrong field, so RabbitMQ refused the 250 KB message as malformed.

Azure Blob (internal/azblob) against Azurite, which checks Shared Key signatures as the service does - a wrong key is refused with 403: put, list, get and delete, a name with a space (first stored double-escaped, a%20b, until fixed), and an access tier read back from the blob. A channel took blobs from one container to another, moving the good one to processed/ and the unparseable one to error/.

Azure Service Bus, against Microsoft's Service Bus emulator (with Azure SQL Edge beside it): the same send, release, redeliver and accept run passed unchanged, signing in with SASL PLAIN as the policy name and key, as Service Bus itself accepts.

What this does not show: Service Bus in Azure, over TLS on 5671; Event Hubs; SAS tokens against Azure.

MQTT, IBM MQ and Google Cloud, against Mosquitto, IBM MQ 10 and Google's emulators

Four connectors added together, each tested against the real software or the vendor's own emulator, and each in a channel that read from one place and delivered to another, leaving nothing behind at the source.

  • MQTT (internal/mqtt, written to the MQTT 3.1.1 standard), against Mosquitto 2.0 with a password file and persistence: a wrong

password refused by the broker; a persistent session subscribed and left; three messages published while it was away, one 260 KB; the session returned and took the first without acknowledging it (a failed handling); on the next return the broker sent it again, then the other two, in order.

  • IBM MQ (internal/ibmmq, the messaging REST API), against IBM MQ Advanced for Developers 10.0.0.0, built for arm64 from IBM's

mq-container repository because IBM's published amd64 image will not start under emulation: put, browse twice (the same message, not removed), remove by message id, removing it again reported as gone, a 260 KB message, and a wrong password refused with 401. The channel spoke TLS and checked the queue manager's certificate against a CA file, as a site would.

  • Google Cloud Pub/Sub (internal/gcp), against Google's Pub/Sub emulator: publish with attributes and an ordering key, pull, a

nack redelivering the same message, an ack leaving the subscription empty.

  • Google Cloud Storage, against fake-gcs-server: multipart upload with a storage class, list, get (a name with a space, a 650 KB

object, one refused over the size limit), delete, deleting again not an error. The channel moved the object to processed/.

  • Service account sign-in: the JWT Google's token endpoint takes, checked by a test endpoint that verifies the RS256 signature,

the key id, the scope and the audience, and the token reused until it nears expiry.

What this does not show: Google Cloud itself (the emulators take no credentials, so the signed sign-in is held only to the test endpoint); fake-gcs-server does not page a listing, so paging is untested; AWS IoT Core and Azure Event Grid's MQTT broker; IBM MQ on z/OS; the REST API's v1 and v2.

What was checked.

scripts/shl-interop.sh runs a Linux build of Perfuse beside vintasoftware's kill-the-clipboard TypeScript library, an independent implementation of SMART Health Links STU 1 and SMART Health Cards 1.4. The library's SHLViewer resolved a passcode-protected link Perfuse hosted - embedded, and by one-time location - and got the bundle back byte for byte; a wrong passcode raised the library's own SHLInvalidPasscodeError. The other way, the library built a link with SHLManifestBuilder, including a SMART Health Card it issued and signed, and Perfuse's /api/shl/resolve decrypted both files and decoded the card (not verified, correctly: its issuer was plain HTTP).

The SMART Health Cards specification's own example card verifies against the example issuer's published key, from its JWS, its numeric QR form and its file form; altering one character of the payload stops it verifying (internal/shl/testdata, offline).

The QR encoder is written here, so scripts/qr-check.sh decodes its output with zbar, an independent reader: 26 codes from 1 to 1,800 bytes at both error correction levels, all read back exactly. The code on the share screen was photographed by the browser test and read by zbar too.

What this does not show: a patient's phone app reading a Perfuse link over the internet, and verification against issuers in the VCI directory.

Eligibility (270/271), claim status (276) and enrolment (834)

What was checked, and what could not be.

Every built 270 and 276 is parsed back by Perfuse's X12 reader and passes its envelope check - SE01 segment counts, ST/SE and GS/GE control numbers, ISA/IEA. The synthetic 271 used in the tests and the browser was itself caught by that check on its first run: it declared 21 segments and held 23.

The independent check available was pyx12 4.0.0, the open-source HIPAA validator. It accepts the 834 test file as valid against 005010X220A1. It ships no maps for 005010X279A1 or 005010X212, so it could not check the 270, 271 or 276; those were built to the 5010 implementation guide structure and have not been checked against a TR3-based validator or accepted by a payer.

The CORE check is this program's reading of the CAQH CORE Eligibility & Benefits data content rule. It is not CORE certification.

S3, SQS and SNS, against LocalStack and AWS's signature example

What was checked: the five AWS connectors end to end, with the message looked for where it should have arrived.

The signer (internal/awsv4, shared by every AWS connector) reproduces the signature in AWS's own worked example from the Signature Version 4 documentation, and the signing-key derivation vector. LocalStack 4 does not verify signatures by default, so those two tests are what hold the signature; LocalStack holds everything else.

Against LocalStack: SQS send, long-poll receive and delete; a FIFO queue keeping order within a patient's group and dropping a resend with the same deduplication id; an SNS publish arriving in an SQS queue subscribed to the topic; S3 listing with start-after, a key containing a space, get with a size limit, copy and delete; and an object written with GLACIER_IR reporting that storage class back. Then two running channels: SQS in, to an SQS queue and an Athena-shaped archive in S3, checked in the destination queue, in the source queue (empty, so the message was deleted after handling) and in the bucket (one JSON line under dt=<today>/ with the fields parsed); and S3 in, where a good object moved to processed/, an unparseable one to error/, and a .txt the suffix excludes stayed put.

The first run found that an unparseable S3 object was moved to processed/: the channel reports a message it cannot parse without returning an error, so the source now checks the outcome as the file source does.

What this does not show: behaviour against AWS itself, IAM policy failures, KMS-encrypted buckets, and the perfuse athena table run in a real Athena.

FHIR, against HAPI FHIR

What was found: the converter was fabricating an identifier system, and HAPI did not object to it.

A converted Patient is accepted by a real HAPI server, and the round trip preserves what matters: the name, the birth date converted from HL7's 19061209 to 1906-12-09, and the gender mapped from F to female.

The third test is the one that makes the other two mean anything. It posts deliberately invalid FHIR and requires HAPI to refuse it, because a server with validation switched off would make the first two tests pass while proving only that a server exists.

Da Vinci PAS 2.2.1, against the official HL7 validator

What was found: the 278-to-PAS conversion had never been checked against PAS, and it failed.

pas278 converts an X12 278 decision into a PAS ClaimResponse. Until this check, its only evidence was its own tests, which asserted what the code did rather than what the guide requires.

The official HL7 validator (validator_cli.jar, loaded with hl7.fhir.us.davinci-pas#2.2.1 and its dependencies) rejected every output, with six errors for a certification and eight for a pend:

  • patient, created, insurer and request were missing, and all four are required.
  • The adjudication category was not the pattern PAS fixes, submitted, and the code used instead, PASTempCodes#reviewAction, does not exist.
  • A pend answered outcome: queued, which is not in PAS's ClaimResponseOutcome value set.

Reading the profile and the guide's own published example (ReferralAuthorizationResponseExample) found two more things the validator could not see: the reason code named the wrong X12 code list, and processNote.type was shaped for R5 rather than R4.

After the fix, a certification, a denial and a pend each validate with 0 errors. The warnings that remain are the validator reporting that it cannot load X12's code systems, which X12 licenses.

The validator needs Java and a large download, so it is not run by make check. TestClaimResponseMatchesPASPublishedShape holds each rule it enforced instead, and fails if one is reverted.

What this does not show: that any payer's PAS endpoint accepts these responses. Conformance to the profile is a necessary condition, not a sufficient one.

CARIN Blue Button 2.2.0 and PDex 2.2.0, against the official HL7 validator

What was found: five conformance defects in the new converters, and a store that had been dropping most of every ExplanationOfBenefit.

cms0057 converts paid claims (an 837 with its 835) into CARIN Blue Button bundles, and Da Vinci PAS decisions into PDex prior authorisations. Both were run through the official HL7 validator (validator_cli.jar, FHIR 4.0.1) loaded with the published packages hl7.fhir.us.carin-bb#2.2.0 and hl7.fhir.us.davinci-pdex#2.2.0. The CARIN inputs were synthetic professional, inpatient and outpatient claims; the PDex inputs were the PAS 2.2.1 guide's own example Claim and response bundles, not shapes written here to agree with the converter.

The first run rejected all three CARIN claim types:

  • The institutional profiles fix the claim-type coding *with its code system version* (1.0.1), and the converter wrote no version.
  • CARIN's EOB invariants require meta.profile to carry the profile version (|2.2.0).
  • The CARIN Patient requires the member identifier to have a system. The converter left it out when none was configured; it now refuses to convert without one rather than inventing a namespace.
  • The outpatient profile allows a service date, not a period, on a line.
  • Two test NPIs failed their check digit. The fixtures were wrong, and the converter now reports a bad NPI in words before US Core rejects it.

After the fixes the validator reports no errors on any of the three CARIN claim types or on either PDex decision. The remaining warnings are a missing narrative (a best-practice recommendation) and NUBC and X12 code systems the validator has no copy of, so cannot check codes in. The only PDex errors on the first run were example.org URLs carried in from the HL7 example inputs, which the validator refuses in non-example content; with those replaced it reports none.

The larger finding came from asking whether a converted claim survived the FHIR server. It did not: the store parsed every resource into an internal model and serialised the model, and the model declared a dozen of ExplanationOfBenefit's fields. Items, diagnoses, supporting information, adjudication and payment were dropped on the way in - everything a CARIN claim is for - while the package's own comments said unknown members survived a round trip. The model now keeps whatever a resource carries that it does not declare, without bringing back a declared field the program cleared on purpose, and a test loads a CARIN bundle through the transaction endpoint and reads it back whole.

Asking who could call a Group export found the next gap: any valid token could export any Group, so a provider given a token for its own attribution list could read every other provider's. API tokens can now be limited to Groups (perfuse token create -fhir-groups). TestGroupLimitedTokenReachesOnlyItsGroup holds that such a token exports and reads its own Group, gets not-found for another provider's, and is refused ordinary reads, searches and $member-match; TestAPITokenGroupLimit and TestCreateTokenWithFHIRGroups cover storing the limit and issuing it through the console API.

Not verified: the member match and Group exports against another payer's implementation, and the metrics against a payer's published figures.

HL7 v2 to US Core 9.0.0, against the official HL7 validator

Five synthetic v2.5.1 messages (internal/v2fhir/testdata/uscdi: ADT^A01 with a diagnosis and an allergy, ORU^R01 with a specimen, VXU^V04, MDM^T02, SIU^S12) were converted with -us-core. Each resulting resource was then validated against US Core 9.0.0 with the official HL7 validator, using the live terminology server. US Core 9.0.0 is the version ONC's 2026 SVAP approves for USCDI v6.

The first run found that -us-core had been claiming almost nothing. Perfuse's own checker knew only US Core Patient, so every other resource went out without a claim. Validating those resources against the profiles they should have met found errors in the converter itself:

  • A Location's partOf named the facility Organization, which is invalid FHIR (partOf is another Location). It is now

managingOrganization.

  • The facility Organization had no active, which US Core requires.
  • A Specimen's type was read only from OBR-15, which v2.5 withdrew. A 2.5.1 result's SPM segment was ignored.
  • The sender's code text was written as the coding's display, so LOINC and CVX codes carried displays their code systems

contradict. It is now the concept's text.

  • A LOINC document type in TXA-2 was labelled as HL7 table 0270.
  • DG1 diagnoses and AL1 allergies were not converted at all. Race, ethnicity and preferred language were dropped. A phone

number in v2.5's split components was dropped too.

After the fixes, Perfuse checks every profile the converter can claim. 23 of the 24 resources claim US Core, and the validator reports no errors on any resource. The 24th is an Encounter with no type, so it carries no claim and the conversion notes say why. The remaining warnings include a missing narrative, observations with no performer, local codes outside extensible value sets, and an inactive RxNorm code in the test data.

TestEveryConformingResourceClaimsItsUSCoreProfile and TestTheUSCoreValidatorFindingsStayFixed hold the result without Java.

What this does not show: conversions of messages from a real EHR feed, or an ONC certification test (Inferno) run.

CRD, DTR and PAS with HL7's Burden Reduction provider reference implementation (24 October 2026)

What was checked: the whole prior authorization round trip, with the EHR side played by software Perfuse's authors did not write. HL7's Da Vinci Burden Reduction provider reference implementation (hlseven/davinci-br-provider, the main image of 5 October 2026, run from its published Docker image) was pointed at Perfuse as its payer, with SMART Backend Services (its own key, registered as a client) and CDS Hooks JWTs it signed itself. Its own workflow was driven in a browser, as its end-to-end tests drive it: start an encounter for its patient pat013, order home oxygen (E0424), sign, submit the prior authorization, complete the documentation it was asked for, and wait for the decision.

What happened, step by step: encounter-start and order-sign were answered with coverage cards and the coverage-information system action; the PAS $submit was pended asking for the plan's home-oxygen questionnaire; the EHR fetched it with DTR $questionnaire-package, rendered it, and returned the answers with CDex $submit-attachment; a reviewer approved it in Perfuse's queue; the PAS subscription notification reached the EHR, which closed its task and showed "Approved" with the authorization number and period.

It found five faults in Perfuse, each fixed and held by a test:

  • Two of CRD's six hooks had no service. encounter-start and encounter-discharge were not offered, and CRD asks a server to

answer every hook (hook-4). They are now, with the encounter prefetched or read from the EHR and answered about by its type.

  • A questionnaire was asked for in a shape PAS does not describe. PAS (ainfo-3) asks for a payer's questionnaire with one

CommunicationRequest carrying LOINC 102089-0, under the line's trace number, which is the DTR context. Perfuse sent no CommunicationRequest and put the questionnaire's URL in the Task, so the EHR, reading the response as PAS says, never saw that anything was asked. Now it sends the 102089-0 request, the trace number as the Task's questionnaire context (a line without one is given one), and DTR answers $questionnaire-package from that context alone. The HL7 validator, with its terminology server, accepts the CommunicationRequest.

  • "communicationRequest": null. A pended answer asking for a questionnaire alone wrote a null, which is not FHIR JSON.
  • A Subscription could not be found by its endpoint. R4's url search parameter (and type, payload) were not supported, so

the EHR's check for its existing subscription was refused before it made one.

  • Documents sent under the request's trace number were refused. PAS maps the Task's identifier to the ClaimResponse's first

identifier, and the EHR sent that as the TrackingId; only the Task's and CommunicationRequests' own identifiers were accepted. Both are now, and the Task carries the ClaimResponse's identifier.

Two things in the reference implementation itself, not Perfuse, needed working round for the run: its UDAP sign-in server (hlseven/fast-udap-security, published for x86 only) crashed under emulation on an Arm Mac, so the EHR ran with its own sign-in off; and its FHIR server serves a search's cached results for 60 seconds, so a new order or questionnaire answer appears on its screens up to a minute late.

What this does not show: a production EHR, or the reference implementation's UDAP security path. Perfuse's UDAP client was tested on its own against the UDAP reference implementation instead (see "UDAP and TEFCA" below).

Da Vinci CRD, DTR and PAS 2.1.0, beside 2.2.x (24 October 2026)

What was found: five places where a 2.1.0 client was answered in 2.2.x's form, and one where DTR 2.1.0 forbids what 2.2.0 allows.

CMS-0057-F recommends CRD, DTR and PAS 2.0.1 and 2.1.0; the 2.0.1 releases rest on US Core 3.1.1, which expired on 1 January 2026. No Inferno kit tests 2.1.0, so conformance/davinci21/run.py sends each 2.1.0 guide's own example requests to Perfuse and has the HL7 validator (6.10.4, terminology off) check every answer against the 2.1.0 packages (hl7.fhir.us.davinci-crd#2.1.0, -dtr#2.1.0, -pas#2.1.0), then makes the same calls naming no version and checks them against 2.2.1, 2.2.0 and 2.2.1.

Before the change every 2.1.0 answer failed: the profiles claimed 2.2.x, DTR's package was named packagebundle (2.1.0 says PackageBundle), its QuestionnaireResponse used qr-coverage (not in 2.1.0), PAS answers carried claimResponseReviewer and the transmission, admission and discharge extensions 2.1.0 does not define, and $next-question refused 2.1.0's parameter name (in). Perfuse now reads the release a caller speaks from the request, then the client's registration (davinci_version), then -davinci-version, decides and stores in the 2.2.x form, and rewrites for a 2.1.0 caller as it sends (internal/davinci, internal/crd/v21.go, internal/fhirserver/pas21.go). The check of the adaptive questionnaire also found that DTR 2.1.0 forbids enableWhen on the items of the questionnaire a $next-question returns (and a url on it, while 2.2.0 requires one): both are now written as each release wants.

After: no errors in Perfuse's answers for either release: CRD's two order-sign examples (the second turns coverage-info off and gets the empty answer CRD asks for), DTR's $questionnaire-package (the guide's input, and an adaptive questionnaire), $next-question and $log-questionnaire-errors, and PAS's five request Bundles and its inquiry. What the validator still reports is in the guides' own examples, returned as sent: example.org URLs; the guide's SubscriberExample patient, which does not meet profile-beneficiary (the guide's own request Bundle fails the same check); a 2.1.0 Coverage with no identifier, which 2.2.1 requires. Inferno's 2.2.x suites were re-run on the change: PAS 82 pass, 2 fail; CRD 92 pass, none failed; DTR 43 pass, 1 omit, 2 fail, the same as before.

What this does not show: a 2.1.0 client from an EHR vendor talking to Perfuse. These are the guides' examples, sent by a script.

Da Vinci CRD 2.2.1 and DTR 2.2.0, against the official HL7 validator

The order CRD updates (a DeviceRequest carrying the coverage-information extension), the whole CDS Hooks response validated against CRD's CRDHooksResponse logical model, the example questionnaire against DTR's dtr-std-questionnaire, and the server's own $questionnaire-package output against dtr-qpackage-output-parameters: no errors. The remaining warnings are a missing narrative on the order and the HCPCS code system, which the validator has no copy of.

The first run found that cards named their topic in CRD's temporary code system, which the response model's value set does not include; they now use the CDS Hooks card-type system. Building the package found that the FHIR store's Questionnaire model declares no item, and the package was written through a path that did not restore undeclared members - so every questionnaire went out with no questions. It now uses the serialiser that does.

DTR value sets and adaptive questionnaires, October 2026. Every ValueSet in DTR 2.2.0 and US Core 6.1.0 (44) was loaded and expanded. The 16 that list their codes expand, and their expansions validate with no errors; the 28 that need VSAC or a whole code system are refused by name with 422, none expanded partly. Building it found that table-view expansions carried no timestamp, which FHIR requires. $next-question was run on DTR's own example input and on a three-step branch, and its output validates against dtr-next-question-output-parameters, the package against dtr-qpackage-output-parameters. That run found a package defect older than this work: a bundle entry's fullUrl was the questionnaire's canonical url even when the url did not end in Questionnaire/{id}, which FHIR forbids; it is now the resource's address on this server. Not verified: a real DTR app (such as a SMART DTR client) driving the adaptive flow.

ELR, October 2026. The old ELR builder wrote a message from a handful of fields, with no SFT, ORC or SPM, and nothing used it; it was removed. perfuse elr was written instead, and its output checked with NIST's HL7 v2 validator (hl7-v2-validation 1.7.2, the engine behind the ONC ELR tool) against the APHL ELR 2.5.1 Foundation profile that NIST publishes with its ELR validation plugins. Two sources: a lab message with a CBC and a SARS-CoV-2 result, and a SARS-CoV-2 result with no specimen. The run is ELR_OUT=dir go test ./internal/publichealth -run NIST, then the validator over dir.

The first run found 32 errors. Fixed: placer and filler numbers without an assigning authority, an NPI without its OID and identifier type, race and ethnicity in CDC codes where ELR uses HL7's tables, the phone number in a component ELR does not allow, and no performing lab on the results. What is left are the lab's own gaps (no specimen type, no time received, both named in the notes) and findings the profile raises against itself:

  • MSH-15 is declared with a cardinality of exactly two, the field's length put in the wrong place ("no valid length specification").
  • PID-10 and PID-22 must be coded HL70005 and HL70189, and the coding system table they are checked against lists only the

placeholder HL7nnnn.

  • OBX-5's coding system is bound to HL7's table 0396 without SCT, while OBX-3 is bound to the ELR table, which has it; ELR

requires SNOMED CT for coded results.

  • SPM-4 is bound to HL7 table 0487 alone, and the specimen was coded in SNOMED CT, which ELR prefers.
  • ELR-037 (ORC-12 must equal OBR-16) fails with the two fields identical, as it does on the profile's own sample message.
  • Two ARLN program checks that do not apply to this program.

The ONC ELR tool, October 2026. The ONC certification tool itself (Electronic Laboratory Reporting HL7 V2.5.1 Validation Tool 1.9.3, formerly NIST's, now at tools.valitheus.com/mu-elr), context-free validation, driven through a browser with the same two messages. It validates against ELR Release 1 with its errata, not the APHL profile. It found one error the profile does not check:

  • MSH-2 was four characters. The October 2011 errata to ELR Release 1 made MSH-2 five, ^~\&# (the separators and the

truncation character), and the certification tool requires it (ELR-013). Perfuse now writes the five. A state whose own guide still shows four can have them with encoding_characters: "^~\\&" in the ELR file. The APHL profile accepts either.

After the fix, the message with its specimen is Valid, 0 errors. The one without a specimen has two: SPM-4 and SPM-18 missing, the lab's gaps Perfuse names in its notes and does not fill. None of the APHL profile's own defects above appear in the ONC tool.

Then the tool's own test data: its 28 example messages (ELR-ExampleMessages 1.9.2, ten test cases from a maximally populated lead result to culture and susceptibility panels, reflex hepatitis panels, titers and multi-result respiratory panels), each sent through perfuse elr as the lab's message, and Perfuse's output validated. The first run found two faults, both in messages with a child order (test cases 4 and 5B, a susceptibility panel or a reflex test whose OBR-29 names its parent):

  • Perfuse built an ORC and an SPM for the child order. The lab sent neither, which ELR allows, so the ORC came out without an

ordering facility and the SPM without a specimen type: six errors in a message that was valid as sent. A child order now goes as the lab sent it, and a child with no trigger code of its own (antibiotics are not on the RCTC) is reported with its parent.

  • A note about the patient was dropped. An NTE after the PID belongs to the patient group; it is now kept there.

Also: when OBR-17 has a callback phone, ORC-14 now carries it, as ELR requires. After the fixes all 28 are Valid, 0 errors. The context-based tab, which compares a message with each test case's data sheet, was not run: its test case tree does not register a selection made by a script.

SMART App Launch, October 2026. Inferno's SMART App Launch STU2.2 suite (smart_app_launch_test_kit 1.0.3) against serve -fhir -smart-clients -smart-users, Perfuse issuing the tokens itself. A script played the browser: it followed Inferno's authorization links, signed in through Perfuse's forms, chose the patient, approved the scopes and returned to Inferno, and for the EHR launch opened Inferno's launch URL from Perfuse's /auth/launch. With a public client: standalone launch, EHR launch, Backend Services and token introspection, 80 of 80. With a confidential-symmetric client: 67 pass and 3 omitted (CORS on the token exchange, which SMART requires only for public clients).

The suite found:

  • No CORS headers. A SMART app in a browser reads discovery, the capability statement and resources from another origin. The FHIR

endpoint and the token, key and discovery endpoints now answer with Access-Control-Allow-Origin: *, which is safe because none of them reads a cookie; every request carries its own token.

  • An app launched from a chart could not read its own user. A token limited to a patient refused the Practitioner named in its

fhirUser. That one resource is now readable; anything else outside the patient is still not.

  • Introspection refused Inferno, which sends no credentials unless told to. It now also accepts an active access token from this

server as the caller's proof, besides a confidential client's credentials; a public client's id alone is still refused.

  • A member's user/ scopes would have reached what a user may see. They are issued as patient/ scopes instead.

The Inferno Da Vinci PAS test kit, October 2026. The official PAS Server v2.2.1 suite (davinci-pas-test-kit v0.15.2, run locally in Docker) against serve -pas -fhir-subscriptions, with Keycloak issuing the SMART Backend Services token: 82 pass, 2 fail, of 84. Approval, denial, a pended request finalised by a reviewer and delivered on the PAS subscription, the four-step claim update sequence, inquiries, error handling, and the must-support groups for every profile but ClaimResponse pass. The two failures are the ClaimResponse must-support checks for $submit and $inquire. The elements they want that Perfuse does not send are not allowed there by their own extensions' context in PAS 2.2.1: adjudication.extension:reviewAction (its reviewActionCode may only appear on item and addItem adjudications), extension:authorizedProvider and item.extension:communicatedDiagnosis. Sending them made the validator reject every response, so they are left out. The rest are request.extension:DataAbsentReason (Perfuse always knows the request) and addItem.extension:productOrServiceCodeEnd.

Run again with Perfuse's own authorization server (-smart-clients) in place of Keycloak, the kit's backend client registered with its own public keys and its token from /auth/token: the same 82 of 84.

The subscription channel header fix, before its release (24 October 2026). The PAS kit's notification checks come from the Subscriptions test kit, which up to v0.13.0 added its own Authorization header to a channel beside the one the kit's sample Subscription already carries (subscriptions-test-kit#48, reported from here). The PAS run has had to drop the sample's header. The fix is on that kit's main branch (commit 0c71363), not in a release yet; run with it (SUBSCRIPTIONS_KIT_REF=0c713638 PAS_KEEP_SAMPLE_HEADER=1 conformance/inferno/run.sh pas) and the sample's header kept, every subscription and notification test passes: creation, handshake, delivery, and the pended request's full-resource notification. Overall 80 pass, 2 fail (the same ClaimResponse must-support findings), 2 skip: the kit then needs inferno_core 1.5.0, under which two must-support checks of the kit's own request data (a Coverage relationship X12 code) no longer find what they found under 1.4.3. Until the fix is released, the recorded run stays the released kit with the sample's header dropped.

The request bundles were the kit's own, for its client simulation. They are identical for approval, denial and pending, because Inferno's simulated payer is told what to answer; for a payer that decides from what is asked, the denial asked for dialysis and the pended request for surgery, and three more requests exercised a quantity limit, an alternative, a request for documents and a line naming no service.

The suite found:

  • The handshake was refused because it arrived about a millisecond before Inferno began listening for it. A refused handshake is

now retried before the subscription becomes an error.

  • PAS's own example Subscription writes its filter as org-identifier=... without a resource type, which Perfuse refused.

The Inferno Da Vinci DTR test kit, October 2026. The official DTR Payer Server v2.2.0 suite (davinci-dtr-test-kit v0.18.0, run locally in Docker) against Perfuse with Keycloak as the authorization server for SMART Backend Services: 43 pass, 1 omitted (no Binary attachments in the data), 2 fail. The same 43 with Perfuse's own authorization server (conformance/inferno/run.sh dtr), once an app launch client is registered: the kit's discovery test asks for authorization_endpoint, which SMART App Launch 2.2 requires only of a server that offers app launch. The two that fail cannot pass for any server: they require Questionnaire.extension:assemble-expectation, which DTR's base questionnaire profile allows at most 0 times, and item in the adaptive search profile, which also has a maximum of 0. The payer data was Inferno's own dinner-order fixtures, an adaptive version of it written with standard enableWhen, and one questionnaire using every element DTR marks must-support.

The suite found these defects, each fixed and covered by a test:

  • The FHIR base ignored -public-url, so bundle links and the SMART audience named the listen address (0.0.0.0).
  • A token with read scopes was refused $questionnaire-package, $next-question and $expand, because every POST counted as a write.
  • The capability statement did not declare the three DTR operations, though they were served.
  • /.well-known/smart-configuration lacked grant_types_supported, and claimed OpenID Connect sign-in with no jwks_uri.
  • $expand refused a Parameters body, which is how FHIR clients send it.
  • Small value sets went into the package unexpanded (DTR oper-15 wants those under 40 codes expanded).
  • Library and value set references in the packaged questionnaire were unversioned; they now name the version packaged.
  • An unknown CRD context answered 200; DTR requires a 4xx with an OperationOutcome.
  • $next-question accepted answers its questionnaire does not have, of the wrong type or outside the options; these are now 400.

It also asked one question per call where the answers already decided more, and its contained questionnaire did not say it was derived from the packaged one.

The Inferno Da Vinci CRD test kit, October 2026. The official CRD Server v2.2.1 suite (davinci-crd-test-kit, run locally in Docker) passes as a whole: discovery, the four hooks, the cross-hook and must-support checks, and the technical issues, no member found, coverage not found and no active coverage responses. It took these fixes:

  • Every signed call was refused. The CDS Hooks JWT has iss, aud, exp, iat and jti and no sub, and the verifier

required a subject, as sign-in does. A JWT no longer needs a subject, and a repeated jti is now refused.

  • Discovery was wrong. There was no davinci-crd.version, and the configuration options had the request-side name and were empty.

Discovery now declares 2.2 and a coverage-info boolean, and turning it off returns no cards.

  • order-dispatch answered nothing. Only a prefetch key called order was read. Orders are now found from dispatchedOrders in

any prefetch, or read from the EHR.

  • Nothing was ever fetched. When prefetch lacked the Coverage, the answer was a card. The Coverage is now read from the EHR's

fhirServer. A failure gives indeterminate with technical, and an inactive or ended coverage gives not-covered with no-active-coverage.

  • Every patient was treated as a member. With members: fhir, the coverage is checked against the payer's records:

no-member-found, coverage-not-found, or satisfied with the number of an approved authorisation.

  • The updated order changed. Re-encoding sorted its keys, and Inferno's comparison, which sorts arrays by their JSON text, saw

an Appointment's participants as modified. The order is now returned byte for byte, plus the extension.

  • A GET of an unknown /cds-services/... path answered with the console's HTML. It now answers with JSON.
  • The example rules used a display that HCPCS rejects. The HCPCS URI is also inconsistent: HL7 Terminology prefers

http://www.cms.gov/..., while CRD 2.2.1 and CARIN BB 2.2.0 use https://. Rules now match either.

The run also turned up two problems in the kit. Its own sample order-select request fails its context check, because CommunicationRequest is listed in selections. Its sample appointment has neither start nor requestedPeriod, which crd-apt1 requires. Both were adjusted for the run. encounter-start and encounter-discharge were not served then; they are now (see below).

Rechecked in October 2026, after reading implementers' questions in chat.fhir.org's Da Vinci channels.

  • DTR had been built to 2.1.0 while CRD and PAS here are 2.2.1, the releases meant to be used together. Against DTR 2.2.0 the

package was not conformant: its parameters were PackageBundle and Outcome where 2.2.0 says packagebundle and outcome, each bundle lacked the QuestionnaireResponse 2.2.0 requires, a Library's own depends-on Libraries were left out (the question raised about the reference implementation omitting FHIRHelpers), a version-specific canonical returned whichever version was found first, context was ignored, and $log-questionnaire-errors and $next-question, which 2.2.0's payer capability statement requires, did not exist. All fixed; the output validates against dtr-qpackage-output-parameters 2.2.0 with no errors.

  • CRD answered an order no rule matched with info-needed OTH and no reason, which breaks crd-ci-q6: the validator rejected every

such answer. The earlier check had only validated a matched order. Rules files are now checked against crd-ci-q1, q2, q3, q5, q6 and q8 when they load.

  • CRD 2.2.1's own invariant crd-ci-q4 rejects every coverage-information carrying doc-purpose withpa, even with pa-needed

auth-needed: its left side is a where() with no exists(), so it is empty, and "empty implies false" is not true. It also tests for noauth where the code is no-auth. Perfuse does not send withpa unless a rules file sets allow_withpa, so its output stays valid. The guide's authors agree the invariant is broken (chat.fhir.org, October 2026) and a technical correction is due.

What this does not show: CRD against a production EHR (Inferno's simulated CRD client is the stand-in), and DTR against a real SMART on FHIR documentation app.

HL7 DSDR signatures, against OpenSSL and xmlsec1

The signing PKI was all OpenSSL: a CA from openssl ca, its OCSP responder from openssl ocsp, and a time-stamping authority from openssl ts -reply. Against those, a C-CDA was signed to XAdES-X-L. Independent software then checked the result:

  • OpenSSL verified both time-stamps, each over the canonical bytes XAdES says it covers, and the embedded OCSP response.
  • xmlsec1 1.3 (libxmlsec) verified the XML signature with the certificate chained to the CA. That covers the XPath Filter 2.0

exclusion of the signers, exclusive canonicalisation, the signed-properties reference and the RSA signature.

  • xmlsec1 refused the document once a dose in it was changed.

The guide was read from HL7's October 2014 publication. The regulation was read from the CMS-0053-F final rule.

Running xmlsec1 against Perfuse's XML signatures for the first time found three faults in the existing signer, which only Perfuse had ever checked:

  • Canonicalisation wrote the line break after the XML declaration. Canonical XML has no text outside the document element, so

no other verifier reproduced the digest. Now byte-for-byte equal to libxml2's.

  • The signed-properties reference named no canonicalisation, so XML-DSig's inclusive default applied while Perfuse digested

exclusively.

  • ECDSA signature values were DER, where XML-DSig carries r and s side by side (RFC 4051). Older Perfuse signatures still verify

in Perfuse.

xmlsec1 now verifies Perfuse's enveloped and by-ID signatures, RSA and ECDSA (internal/xmldsig), and its DSDR signatures (internal/dsdr). Both tests run when Docker is available.

What this does not show: a payer's own DSDR verifier, which none publishes, and a signature from a commercial healthcare CA.

Hosted FHIR sign-in, against Keycloak

The client_credentials preset was run against Keycloak 26: a confidential client created through Keycloak's admin API, the token Perfuse fetched checked to be Keycloak's and issued to that client, and a wrong secret refused with Keycloak's own error, without the secret in the message. The aws preset signs with the shared Signature Version 4 signer, which matches AWS's published example, for the service healthlake in the URL's region. Not verified: AWS HealthLake and Azure Health Data Services themselves, which need accounts.

The 006020 275, against the published companion guides

What was found: one of the two guides contradicts itself about the byte count, and the table is right.

CMS-0053-F adopts the 006020X314 275. Its technical report is licensed by X12 and not held here, so the structure was taken from the two published companion guides for that version: the CMS esMD guide (AR2024.04.0) and UnitedHealthcare's.

  • The binary segment. Both confirm the 006020 binary segment is BDS, with the filter in BDS01, the length in BDS02 and the data in BDS03.
  • The contradiction. The esMD guide's table gives BDS*B64*53* for a payload of exactly 53 bytes. Its sample interchange declares 16 for the same payload. Perfuse follows the table, refuses the sample, and a test holds both.
  • The payload. Both guides show a MIME entity in BDS03, and UnitedHealthcare's gives the ASC and B64 layouts. The parser unwraps it and the builder produces it, and a built attachment round-trips byte for byte, including a document containing every X12 delimiter.
  • Where they differ. The guides disagree on some segments: esMD requires a submitter and REF*X1, and UnitedHealthcare lists a service date. The builder satisfies both where it can and sends an optional segment only when given the value. Each rule is recorded in TestAttachment6020FollowsThePublishedGuides with the guide it came from.

What this does not show: conformance to the technical report itself, acceptance by any clearinghouse or payer, or an electronic signature. CMS-0053-F also adopts a signature standard for attachments, and none is applied.

FHIR subscriptions, and the R4 bug they exposed

What was found: Perfuse's own R4 FHIR server refused Perfuse's own R4 ADT conversion.

Subscriptions notify when an Encounter changes, so testing them meant writing Encounters to an R4 server, and the first write answered 400. The server converted resources to R4 when serving them and did not convert them back when receiving them. So Encounter.class, which R4 writes as a single coding and the internal form holds as a list, could not be read, and it is present in every ADT conversion. The v2-in, FHIR-stored pipeline had never been run end to end against Perfuse's own store.

It is fixed, and two tests keep it fixed:

  • one posts an ADT, SIU, MDM and VXU conversion to an R4 server and requires each to be accepted
  • one requires everything written as R4 to be read back unchanged

Both fail with the fix removed.

The subscriptions themselves were tested against a real HTTP receiver, both in the Go tests and in a Playwright test that drives the interface. The tests confirm:

  • the handshake, and that a client cannot declare its own subscription active
  • that notifications are delivered in order and retried
  • that one written before a restart is delivered after it
  • that a full-resource notification carries the version that changed
  • that redirects are not followed
  • that the page does not show a subscriber's header value or the token in its endpoint

Removing each protection makes its test fail.

What this does not show: interoperation with any third-party subscriber, such as an EHR or a HIE's notification service.

DICOM, against Orthanc

What was found: a second implementation agreeing, where previously there was one.

An instance Perfuse sends arrives at a real PACS, and the patient name, identifier, study date, modality and SOP class all survive the association. The manual already claimed the DICOM work was verified against DCMTK; this is a different codebase agreeing, which is worth more than either claim alone.

The assertion is Orthanc's own catalogue and then the tags it stored — not the absence of an error. An association that negotiates successfully and stores nothing looks identical to success from the sending end.

Databases, against PostgreSQL 16

What was found: nothing broken, and two controls that make the result mean something.

The destination's statement is accepted, the row is in the table, and it is read back over a second connection — because an INSERT that reports success and stores nothing, or that binds its parameters in the wrong order, is indistinguishable from success from the sending side.

Two controls on the dialect itself: a question-mark placeholder is refused by this server and a dollar placeholder is accepted. That makes the tests about the statement rather than about a server that would accept anything. A statement written in the wrong dialect is now refused when the channel is saved rather than at three in the morning.

SFTP, against OpenSSH

Verified against OpenSSH, which is a different implementation from the one used on both ends of the existing tests. A client and server from the same library agreeing tells you they agree with each other.

Hard v2 cases from the FHIR community, against the official HL7 validator

Two deliberately awkward messages posted on chat.fhir.org (#v2 to FHIR, September 2026) as tests for v2 converters, and the 70 messages of the public nw-gmsa/Testing set (NHS lab, genomics, order and Epic messages), converted to R4 with the CLI defaults and checked as whole bundles by the HL7 validator against base R4 with the live terminology server. The first run passed 43 of the 70.

What it found, all fixed:

  • Every fullUrl was malformed: urn:uuid: followed by a resource id, not a UUID. Every earlier validator run had checked the

resources taken out of the bundle, so none of them saw it, and Perfuse's own checker did not look. CDA-to-FHIR bundles had the same fault. They now carry a UUID derived from the resource, so they stay deterministic.

  • The same resource added twice (one practitioner on several results) broke bdl-7 in 13 bundles.
  • Encounters with no class, which R4 requires; coding-system placeholder URIs with spaces in them; and **site identifier

types labelled as HL7 table 0203**.

  • PDFs read as codes. Three messages send a Base64 PDF under OBX-2 CE; it went into Coding.display, past FHIR's 1 MB string

limit. A coded value shaped like ED is now read as ED, and ED becomes a DocumentReference the Observation points to.

  • That fix reached 9 of the 28 such messages (found October 2026, answering a question on the same thread). In 18 the Base64

holds a space, which MIME Base64 ignores and Go's decoder does not, so a complete PDF was refused as undecodable and then still emitted as a Coding with code Base64 and the PDF as its display. Whitespace is now dropped before decoding: 27 become DocumentReferences, and the one whose data is cut short stays text, never a code. The bundles validate with 0 errors.

  • An event that disagrees with MSH-9. A case from the same thread: MSH-9 A01, EVN-1 A08, as an engine that remaps A08 to

A01 leaves it. MSH-9 decides, and the IG maps no EVN-1 row, so the disagreement vanished. It is now a warning.

What the two community messages exposed besides, which the validator cannot see because each was dropped or decided without a note: PID-4 and CX.7/CX.8 dropped; an ISO OID in the assigning authority ignored; repeats of a text result joined with the raw ~; Z segments skipped without a word; MRG on an A08 ignored, and on an A40 too, so no merge was ever expressed; a time of birth in PID-7 dropped; a name type with no FHIR equivalent turned into old; an A08 with a discharge date called in progress; a bare time read as UTC when MSH-7 said +10:00; ug/L not recognised as UCUM; and SN <> (not equal) turned into the number.

After the fixes, 64 of the 70 pass. The other 6 fail only on codes the senders sent that the validator cannot find: a UK-edition SNOMED code checked against the International edition, LOINC answer codes with a typo, and the made-up LOINC codes of a test message. Those are the sender's codes, and a converter that dropped or changed them would be worse. The two community messages are in internal/v2fhir/testdata/hard, with tests holding each fix.

The five US Core messages above were then validated again as whole bundles, with -ig US Core 9.0.0: all five pass. Doing so also caught a mistake in one of our own fixtures, an appointment reason labelled as HL7 table 0277 (appointment type), now corrected.

DSDR signatures, against the EU Digital Signature Service

DSDR signatures were checked with the European Commission's DSS demonstration webapp, built from esig/dss-demonstrations and run in Docker, through its REST validation service. The signature was put back in place of the sdtc:signatureText that carries it, as xmlsec1 is given it. With RSA and ECDSA signers, DSS reports XAdES-XL: every layer is recognised and verifies. The indication is INDETERMINATE / NO_CERTIFICATE_CHAIN_FOUND only because the test CA is on no trusted list. Getting there took three fixes:

  • Perfuse never checked the time-stamping authority's signature on a time-stamp. The test TSA did not sign at all, and the

verifier counted its stamps as XAdES-T evidence, so a forged time would have passed. DSS refused those stamps ("signed by 0 signers"). The verifier now checks the CMS signature, the messageDigest and the ESS signing certificate, and that the TSA's certificate is critically marked for time-stamping and chains to a trusted root. The test TSA now signs as RFC 3161 says.

  • X-L stopped at X. CertificateValues and RevocationValues carried the signer's path only. They now carry the TSA's certificate

and its OCSP or CRL as well, and DSS moved from XAdES-X to XAdES-XL.

  • DSS's one remaining warning is a known choice. The DSDR guide's own SignaturePurpose element sits in

SignedSignatureProperties, where XAdES's schema wildcard is strict. The guide gives the element no namespace, so it is in urn:hl7-org:sdtc. The same purpose is also sent as a XAdES CommitmentTypeIndication, which DSS reads.

The FHIR server, against the Inferno US Core 6.1.0 server suite

The 179 US Core 6.1.0 example resources were loaded into Perfuse's FHIR endpoint. The US Core FHIR API group of ONC's Inferno test kit (run locally in Docker) was then run against it, with a Perfuse API token. The first run passed 225 tests and failed 121; the last passed 323. What it found, all fixed:

  • Every multiple-or search matched nothing. A comma list (status=final,amended) was read as one literal value, and a repeated

parameter was ORed. FHIR defines the reverse, and a test said otherwise, so it was asserting the bug. Commas now OR and repeats AND.

  • POST _search answered 415. FHIR requires search by POST with a form body.
  • The capability statement needed a token. Clients and certification tests read it before they have one. It is now public; it

holds no patient data. It also gained instantiates us-core-server and each type's US Core supportedProfile, and lost an empty interaction array on ValueSet, which FHIR forbids.

  • _revinclude=Provenance:target was refused on every type, and _include=MedicationRequest:medication was not offered.
  • Seventeen US Core search parameters answered 400, among them Condition asserted-date and abatement-date, Encounter

location, type and discharge-disposition, CareTeam role, DocumentReference period, Goal target-date, the Location address parameters and Patient death-date.

The 19 failures left are in the data, not the server. Most are the examples themselves failing current terminology: example.org URLs, LOINC and CVX displays that have since changed, and a CPT code (99201) that was deleted. The rest are elements no example carries (PractitionerRole, a data-absent-reason), and an Inferno multiple-or check that looks for values held only by resources outside the encounter it searched. The SMART launch and granular-scope groups were run later against Perfuse's own authorization server, below.

US Core 7 with SMART granular scopes, against the Inferno US Core test kit

October 2026. The Inferno US Core test kit (us_core_test_kit 1.1.6, US Core Server v7.0.0, SMART App Launch 2.0.0), run locally in Docker against serve -fhir -smart-clients -smart-users, Perfuse issuing the tokens itself. The data was Inferno's own reference server patient bundles (740 resources, patients 85 and 355). A script played the browser through Perfuse's sign-in and consent pages.

  • Standalone launch: 20 of 20.
  • Granular scopes 1 (Condition encounter-diagnosis and health-concern, Observation laboratory and social-history):

the launch 20 of 20, and every repeated search returns only what the granted scopes allow, 17 passes and 2 skips.

  • Granular scopes 2 (Condition problem-list-item, Observation vital-signs, survey and sdoh): the same, 17 passes and 2 skips.
  • The skips are searches the data cannot exercise: no asserted-date search, and no health-concern or problem-list-item

Condition for the patient.

  • US Core FHIR API under a patient token (patient 85): 285 pass, 2 fail with -fhir-patient-directory-search (26 October 2026;

272 pass, 14 fail without it, below).

  • The same group as a backend-services client (system/*.rs, both patients): 503 pass, 4 fail (501 before the 26 October rerun). All four are one Observation in

Inferno's data whose effectivePeriod has stop where FHIR has end; Perfuse returns what it was given.

The suite found:

  • POST _search needed write access. A token that could read was refused a search sent by POST. _search is now a read.
  • The next page lost the search. A paged result's next link kept _count and _offset and dropped every search parameter,

so following it paged through every resource of the type. The link now carries the query.

  • An attachment whose data is absent for a stated reason failed us-core-6. _data holding only a data-absent-reason counts as

data under FHIRPath, and now does here.

  • The fhirUser scope did not let an app read its user. With only granular Observation scopes, the app could not read the

Patient it was told had signed in. That one resource is now readable with fhirUser.

  • A patient's app could not read what its records point at. A member's Coverage (whose patient is beneficiary) and the

Organization, Practitioner, Location and Medication referenced by their records answered 404. Now Coverage counts as the patient's. The shared directory and drug types can be read by id when the scopes cover the type.

  • _include=MedicationDispense:medication was refused. It is now supported, and stored dispenses are indexed again.
  • Most types were answered without meta.versionId or meta.lastUpdated. Only nine types were stamped when stored, so a

Condition, a MedicationRequest or a Coverage gave a client nothing to put in If-Match or a _lastUpdated search. Every stored type is stamped now, and the sender's security labels, tags and source are kept (they were dropped on those nine).

  • A transaction to the base URL was redirected. POST /fhir answered 307 to /fhir/, which most HTTP clients do not follow

with the body. The base itself is now served.

What is left under the patient token:

  • Searches of the directory itself (12: Practitioner, PractitionerRole, Organization, Location by name, address or

specialty). By default Perfuse refuses them under a token limited to one patient, so that a patient's app cannot list every clinician. Let a patient's app search the directory (-fhir-patient-directory-search, Settings → FHIR → Access) allows them, only for the directory and drug types and only within the token's scopes. With it on, all 12 pass (rerun 26 October 2026: 285 pass, 2 fail).

  • What patient 85's data does not hold (2): its one data-absent-reason is on a Condition category no US Core search reaches,

and its documents name no custodian. Both pass with patient 355 in the backend-services run. Inferno's test data, not a Perfuse fault: nothing to fix on our side.

  • The four backend-services failures are one Observation in Inferno's data whose effectivePeriod has stop where FHIR

has end. Perfuse returns what it was given, and the validator rightly refuses it.

Public health case reports, against the HL7 validator and HAPI FHIR

The eICR builder had been in the repository for months, reachable from nowhere, and was wrong in every way the validator could see:

  • fullUrls like urn:uuid:patient-1, which are not UUIDs;
  • references that matched no entry;
  • no Bundle identifier or timestamp;
  • a Composition with none of the seven sections eicr-composition requires;
  • a "Reportable Condition" section coded as Social history.

The README listed eCR and ELR as features. Nothing reached either one.

It was rebuilt to work from what a converted v2 message holds. An admission with a COVID-19 diagnosis, a lab result for SARS-CoV-2 RNA, and the same result with no visit number were each checked with validator 6.10.4 against hl7.fhir.us.ecr#2.1.2, with tx.fhir.org for terminology. All three eICRs, and the eCR message wrapping one, have 0 errors. The runs found:

  • Displays that LOINC rejects. Two section codes used the older "Narrative" wording. The trigger flags copied the sender's text

as the code's display, the same mistake US Core validation found earlier. A trigger now carries a display only when the value set gives one.

  • eCR's absent-reason rule. eCR fixes the data-absent reason to masked, which would say the facility withheld data it never

had. A missing race is sent as the text "Unknown" instead, and a missing language as BCP 47 und.

  • Facility details. eCR requires the facility's phone and address and the clinician's PractitionerRole, which no v2 message

carries, so the facility is configuration.

  • An NPI in XCN.9 was dropped. That covers both NPI&2.16.840.1.113883.4.6&ISO and a plain NPI. The practitioner identifier

ignored the OID handling patient identifiers had, so the US Core and eCR practitioner profiles both failed. It now gets http://hl7.org/fhir/sid/us-npi. The SSA OID likewise maps to us-ssn.

  • PV2-3, the admit reason, was never mapped. The Encounter struct had no reason field it serialised. That field is now there,

and R5 output carries it as reason.

HAPI FHIR (latest) stores each eICR as a document Bundle. HAPI does not implement $process-message, so the eCR message itself was checked by the validator and by a test receiver, not by an agency's endpoint.

The Reportability Response, October 2026

The other half of the exchange: the agency's answer. Perfuse now reads Reportability Responses (serve -ecr-responses) and can play an agency that writes them (serve -ecr-agency). Checked three ways:

  • The IG's own example. The eCR 2.1.2 RR example (one condition, one agency) reads back as the IG describes it: processed

with a warning, Zika reportable on the patient's home address within 24 hours, immediate action required.

  • The HL7 validator. The RR the test agency writes, and the eCR message carrying it, have 0 errors against

hl7.fhir.us.ecr#2.1.2. The first attempt had four: no Composition.encounter, which the RR composition requires, and then the eicr-encounter profile's required location, which the copied encounter must keep along with the resources it points to.

  • Two Perfuse servers. An ADT with a COVID-19 diagnosis went over MLLP to a hospital server whose ecr destination sent the

eICR to a second server playing the agency. The agency answered and posted the RR back; the hospital stored it at the address it had logged when the report went, and the stored RR validated with 0 errors.

The two-server run found a defect unrelated to eCR: bearer_token: ${VAR} on FHIR, CDA and HTTP destinations was sent as the literal text, though the configuration's own comment recommends that form; only the S3, AMQP and cloud credentials expanded it. The agency refused the hospital with 401. Those tokens, and the HTTP destination's password, are now read from the environment.

What this does not show is an agency's endpoint accepting Perfuse's eICR. AIMS onboarding is through APHL and a jurisdiction.

Vendor-shaped v2 from two national programmes, against the HL7 validator

These are not a live EHR feed; no site has run one through Perfuse. For the site that does, perfuse fhir convert -report writes the result in counts and paths only, nothing from a message, so it can be shared. They are the nearest public equivalent: 81 messages from the French national agency's IHE PAM-FR and document-exchange examples (ansforge/hl7V2-exemples) and the NHS Wales v2 examples (GIG-Cymru-NHS-Wales/hl7-v2-examples), covering ADT, ORU, MDM, SIU and VXU in v2.3 to v2.5.1. All 81 converted, and the HL7 validator found 34 errors in the bundles. The fixes:

  • Dates that are not dates. 01/10/1948 and 196203520 became the FHIR dates 0110-19-48 and 1962-03-52. A value that is

not a v2 DT/DTM, or has no such month or day, is now dropped with a note.

  • Codes labelled with HL7 tables they are not in. SPOUSE^^HL70063 was sent where table 0063 says SPO. Codes claiming tables

0063 and 0131 are now checked against them, as table 0203 already was.

  • Table 0189's letters read as CDC codes. N^Not Hispanic or Latino^HL70189 became CDCREC code N, which does not exist. H and

N now map to 2135-2 and 2186-5.

  • An unsystemed LOINC document type was labelled table 0270. TXA|1|18748-4 is now read as LOINC when the code has LOINC's

form and a valid check digit, and kept with no system otherwise.

  • An appointment with a start and a duration but no end broke app-4. The end is now the start plus the duration.
  • CVX 3 for 03 is padded to the form CVX defines.

The one error left was a US Core extension the base-R4 run could not resolve, because that run was made without the US Core package (see below).

A third set followed: the 139 v2 samples Microsoft ships with its FHIR Converter (microsoft/FHIR-Converter at 70fd328e, data/SampleData/Hl7v2, MIT), written in the shape of US EHR feeds: ADT A01 to A60, BAR, MDM, ORU, OUL, OML, OMG, REF, RRI, SIU, VXU and LRI lab messages, v2.3 to v2.8. All 139 converted. The validator, this time with US Core 6.1.0 loaded, found 8 errors, and three were Perfuse's:

  • ICD codes without the dot. DG1|1|I9|71596^...^I9 is how feeds commonly send ICD-9-CM 715.96; the undotted form is not a

code in either ICD system. ICD-9-CM and ICD-10-CM codes sent without the dot now get it, with a note.

  • Vital signs labelled laboratory. Every OBX became category laboratory, heart rates included. Results coded with the

LOINC codes of FHIR's vital-signs profiles are now category vital-signs.

  • A repeated numeric value kept with the repeat separator. 27~25 in a numeric OBX-5 became the text "27~25". One

Observation holds one value, so the repeats are kept as text, "27; 25", with a warning.

The other five are the samples' own: a practitioner's assigning authority &2.8&ISO, which the validator does not accept as an OID (twice), and a "Bacteria identified" result sent with the LOINC heart-rate code, two numbers and no time, which the R4 heart-rate profile then rejects three ways. Perfuse passes such data through with notes rather than rewriting it.

The bundles also draw one warning per in-bundle reference: entries have urn:uuid fullUrls while references are Patient/id, which bundle resolution rules cannot follow inside the file. That is how a transaction is meant to be read (each entry is a PUT to Patient/id, so the reference resolves on the server). The library's Options.BaseURL gives absolute fullUrls that also resolve in the file; perfuse fhir convert has no flag for it.

Re-run with US Core loaded, the French and Welsh set is now free of validator errors as well: the US Core race extension the base-R4 run could not resolve validates.

VPN tunnel state, against strongSwan 5.9

October 2026. Two strongSwan 5.9.13 gateways in Docker (Alpine 3.20) with an IKEv2 tunnel between them: AES-256, SHA-256 and MODP-2048 for IKE, AES-256-GCM for ESP, 10.1.0.0/16 to 10.2.0.0/16. Perfuse read the tunnel with the real swanctl --list-sas (scripts/strongswan-up.sh, then go test ./internal/vpn -run StrongSwanForReal). It reported the tunnel up, the other gateway's address, the networks and the proposals, and no mismatch against a partner sheet that agreed. A sheet saying DH group 19 was reported as a mismatch. With the other gateway stopped and the SA torn down, the tunnel read down.

The output matched the sample the unit test had been written against, so the reader needed no change. AWS and Azure are read from their APIs, tested against recorded answers only.

Mutual TLS, against OpenSSL

Verified against OpenSSL rather than against Perfuse's own client. Certificate requirements, rejection of an untrusted client certificate, and rejection of a certificate for the wrong name are all checked by a tool that shares no code with the thing being tested.

UDAP and TEFCA, against a real authorization server

A real authorization server reads what this client signs. It refuses on trust-community membership rather than on the form of the request, which is the distinction that matters: a server that rejected malformed requests would say nothing about whether the signed metadata was correct.

Discovery and signed-metadata verification are checked against a real server and a real trust community.

The whole flow, October 2026. The UDAP .NET reference implementation's tutorial (udap-dotnet, Duende IdentityServer with Udap.Server, and a FHIR server publishing UDAP metadata) was built from source and run in Docker on an Arm Mac. Its published images are x86 only, as the Da Vinci reference implementation's are. Perfuse was given a client certificate issued from that community's sub-CA, as a member's would be, and ran the business-to-business flow end to end (PERFUSE_TEST_UDAP_DEVDAYS, internal/udap/devdays_test.go):

  • the server's signed metadata verified against the community root;
  • dynamic registration accepted the software statement and returned a client ID;
  • a token request with the hl7-b2b extension (organisation, purpose of use) returned a token;
  • the FHIR server's authorization check let that token through, and turned away a forged token and a request with none.

The tutorial's demo FHIR server then fails on every read in its own search indexer (a library version mismatch inside it), so the resource itself could not be shown. Two things in the tutorial had to be worked around: its authorization server could not create its database as the image's user, and its SDK pin was older than the current .NET 10 image. What this does not cover: the authorization-code (consumer-facing) flow and Tiered OAuth, which Perfuse's client does not do; and TEFCA's own communities, whose certificates come only from QHIN onboarding.

A channel's TEFCA query, October 2026. The same servers, through the Facilitated FHIR exchange a channel's tefca destination uses (internal/tefca/devdays_test.go, same variable). The authorization server's metadata options were set to support and require hl7-b2b, so a token without a purpose of use is refused. The query went with a TEFCA Exchange Purpose code and its token was accepted; the tutorial FHIR server then answered 500 from its own data layer (a MissingMethodException inside the tutorial), as above. Two defects were found and fixed on the way: every query result came back empty (the Bundle was decoded into a struct with an untagged field), and purposes were sent in a code system TEFCA does not use.

Prior authorization requests sent, against the Da Vinci PAS reference payer

The HL7 Da Vinci PAS payer reference implementation (hlseven/davinci-prior-auth, arm64 image, port 9015), with its own request example bundle-prior-auth.json (Apache-2.0, kept as internal/priorauth/testdata/pas-ri-request.json). Set PERFUSE_TEST_PAS_RI=http://localhost:9015/fhir and PERFUSE_TEST_PAS_RI_TOKEN to the test token in its source (AuthUtils.ADMIN_TOKEN). internal/priorauth and internal/engine submit the Bundle and read a pended decision with the payer's preAuthRef; web/e2e/use-pas.spec.ts sends it through a running channel and finds the answer on the CMS-0057 screen. The reference payer pends every request until it is released by hand, so approval and denial are checked against stand-ins built from PAS's review action extension. Found: the earlier client rebuilt a bare Claim referencing resources the payer never received; it now sends the Bundle as built.

Mirth's X12 XML, against Open Integration Engine 4.6

OIE 4.6's own X12 serializer (its datatype-edi extension, run by a small Java harness outside the repository, PERFUSE_TEST_OIE_X12=path/to/oracle.sh) converted 25 generated interchanges (varied delimiters, segment terminators with and without newlines, non-ISA input) and 4 kept examples. TestAgainstOIE requires Perfuse's XML to be the same bytes as OIE's, and the X12 OIE writes back from it to match too. The earlier converter wrote an XML of its own invention, readable by nothing built for Mirth.

The patient-data access monitor

Checked in tests against the server's real audit records: every read the monitor counts is an action the server audits (TestEveryWatchedReadIsAnActionTheServerAudits), and reads through the API reach the screen (web/e2e/access-watch.spec.ts). The baselines (usual rate per hour, daytime-only channels) are statistical and were tuned on generated traffic, not a hospital's; expect to adjust thresholds in the first weeks. Found on the way: saved alert rules were never applied to the running evaluator, although the screen said they were; and on a multi-tenant server the monitor was not given to tenants' channels.


The general lessons, which cost more than the specific fixes

Self-agreement is not evidence. The SAML canonicaliser, the Mirth exporter's nine tests, and the hand-written Mirth fixture were all instances of code agreeing with tests written against the same assumption. In each case the body of passing tests was what made the fault invisible.

Acceptance is not verification. Mirth stores an unusable channel and returns success. An INSERT can report success and store nothing. A DICOM association can negotiate and discard. In every case the sending side sees exactly what success looks like.

A test can hold a defect in place. One test asserted the malformed destination form that caused Mirth to empty the destinations. It was not a missing test; it was a test that had to be deleted.

Controls decide whether a result means anything. The HAPI suite would have passed against a server with validation disabled without the test that requires a refusal. The PostgreSQL suite would have passed against a server that accepted any dialect.

Knowledge recorded next to the wrong file does not propagate. The Mirth enabled fault was written down in a fixture comment in August and the code it described was never changed. Prose is not a guard.

A guard is verified by breaking the thing it guards. A test that has never failed is a hypothesis. When breaking one, confirm the break compiles — a change that does not compile looks exactly like a passing test, and that has caught this project more than once.


The footprint figures on the front page

Measured, not estimated, on one machine with both products idle.

The published figure is 45 MB, rounded up from the latest measurement so that ordinary growth between releases does not make it untrue. Measured 2026-10-28 on an Apple M4: 40 MB resident at start, 42 MB after thirty seconds idle, one MLLP channel, message storage and the FHIR server on, CPU at idle 0.0% (macOS's own phys_footprint was 21 MB; resident memory also counts the program file's shared pages, and the program has grown to about 106 MB as features were added).

The first measurement, in October 2026, was 31 MB resident at rest, 32 MB after thirty seconds idle and 46 MB after five thousand messages had been parsed, stored and delivered, with four channels configured.

Mirth Connect 4.5.2, in a container on the same machine: 383 MB, of which the Java process itself accounted for 358 MB. CPU at idle 0.13% to 0.27%. Its application directory is 254 MB and it requires OpenJDK 17 underneath.

What that comparison is and is not. It is an honest measure of what each costs you to have running, taken the same way on the same hardware. It is not a throughput benchmark, and neither product was tuned: Mirth's memory is largely JVM heap and can be configured down, and Perfuse's will rise with traffic and retention. The durable claim is not the ratio (about eight to one today), it is that one of them needs a Java runtime installed and the other is a file.

For completeness, the throughput observed while measuring was 245 messages per second over a single MLLP connection with an acknowledgement awaited for each one. That figure is bounded by the sequential sender, not by the engine, and should not be quoted as a capacity.


Patient data in this repository

There is none. Every HL7 message in the tree is invented, and there are a few hundred of them because an HL7 engine cannot be tested without messages or documented without examples.

The patient names are recognisable placeholders (DOE, SMITH, literal SURNAME^GIVEN), invented recurring patients so a reader can follow one person through a scenario, historical figures, and names chosen to exercise character handling — accents, non-Latin scripts, apostrophes, and a surname long enough to test field limits. Identifiers are sequential (MRN1, 0001234). The only social security numbers present are the canonical examples that cannot be issued. Telephone numbers use the 555 exchange reserved for fiction.

That is checked by a test rather than by care. internal/compliance/nophi_test.go holds every patient surname in the tree to an allowlist, so introducing a new one is a deliberate act visible in review rather than something that arrives attached to a bug report — which is the realistic accident, because the easiest way to reproduce a defect is to paste the message that caused it, and in this domain that message is somebody's medical record. It also rejects social security numbers outside the canonical set and telephone numbers outside the 555 range.

Break-verified with a fixture containing a plausible name, address and telephone number: both checks failed, naming the file.


What continuous integration found the first time it ran

The workflow file had existed for some time and had never executed, because the repository had no remote. Its first run failed, and what it found was not in the code.

The local gate and the CI gate disagreed about what passing meant. The Makefile allowed each package 300 seconds; the workflow duplicated the test command with its own 120-second budget. So the suite was green locally and timed out in CI, and the failure read panic: test timed out beside whichever test happened to be in flight - which was not where the time had gone. The timeout is now defined once, in the Makefile, and the workflow calls that target.

Then the real finding. internal/engine took 215 seconds, and six of its tests took exactly 15.0 seconds each. An exact round number is a clue: the default retry policy is five attempts with a doubling one-second backoff, which is 1+2+4+8 seconds, and each of those tests was sitting through the full production retry schedule in order to observe a failed delivery it already had. Setting one attempt in the two test helpers concerned took the package from 215 seconds to 93, and to 128 with the race detector on.

What remains slow is legitimate: file and SFTP polling tests that have to watch a file stop changing, which means real time has to pass. Those were left alone.

And a genuinely flaky test, found only because the suite ran on a busy machine. TestWorkerRetriesThenSucceeds waited for a successful send and then cancelled the worker, while asserting on state the worker writes to the store *after* sending. Cancel in that gap and the item is still recorded as sending with three attempts. It failed about one run in six, and only in a full run. It now waits for the state it asserts on.

The first diagnosis of that one was wrong. The fake sender counts successful sends only, so waiting for a count of one was correct; changing it to four made the test fail every time, which is how the mistake surfaced immediately rather than becoming a second flake.


Every view, before v0.1.4

What was found: a view could not be linked to, the console was unusable on a phone, and the test suite was overstating what it covered.

Before v0.1.4, five new probes (web/e2e/hunt-*.spec.ts) drove all 24 views. Each probe asks one question:

  • Roles. Does each view work for a viewer and an editor as well as for admin?
  • Phone width. Does anything run off a 390-pixel screen?
  • Reload. Does a view survive a reload?
  • Hostile text. Does every field survive empty, garbage, wrong-format, Unicode and 400 KB input, and does every primary action then either show a result or say why not?
  • Accessibility. Do all controls and fields have names, do images have alt text, are ids unique, and does keyboard focus stay visible?

Two more probes read every API route from the Go source. One sends each route malformed JSON, the wrong types, nulls and 3 MB bodies, and requires a 4xx in JSON rather than a 500. The other checks each role guard on the running server: 401 with no session, and 403 for a viewer on every editor, admin and platform route. All 128 guarded routes passed, and no escalation was found.

What they found in the product:

  • No view had an address. A reload, a bookmark, a shared link and the back button all led to the Dashboard.
  • Every view scrolled sideways on a phone, for five separate reasons.
  • Revoking an API token that does not exist answered 500, along with four other unmapped store errors.
  • One route answered in plain text where every other /api answer is JSON.
  • A search in Messages that matched nothing looked like one that never ran.
  • Repeated SVG ids could strip the fills from the logo and the flow map.
  • Subscriptions had no way to refresh.

It also found three defects in the tests themselves. Each one made the suite report something false:

  • Six "every view" sweeps had never swept up to four of the views.
  • A run refused by the run lock killed the run it was refused for, which then reported 46 defects that did not exist.
  • The navigation helper confused a view's own tabs with the console's.

The complete itemised list is in the commit message of 36118b7.

Defects found by reading chat.fhir.org

Six months of #implementers, #inferno, #V2, #v2 to FHIR, the Da Vinci streams and others were read for threads where the behaviour being discussed is something Perfuse does. Each candidate was tested against Perfuse before it was called a defect. Four were real, and each now has a test that failed before the fix:

  • Date search compared strings, against a Period's start alone (#implementers, "Period Search"). An Encounter from 29 July to

2 August was not found by date=ge1950-08-01, a time with an offset compared wrongly with one in UTC, and date=2026 was a prefix match. Dates are now indexed as ranges in UTC, Periods whole, and every prefix follows R4. Inferno US Core 6.1.0 FHIR API was rerun afterwards: 323 pass, 19 fail and 111 skip, the same as before, with the failures all in the published example data.

  • A pipe escaped in a search value split the value (#inferno, "Search by identifier containing pipe characters").

identifier=MR0909981\|936\|UNIV OF CA was read as a system and a code and matched nothing.

  • Two contained resources could share an id (#implementers, "uniqueness of contained resource id"). The resource was stored,

and #id then meant either. Such a resource is now refused, on create, update and in a transaction.

  • A CRD card summary could exceed CDS Hooks' 140-character limit (#Da Vinci CRD, "CDS / CRD logical model potential gaps").

The summary includes the payer's rule description. A long one is now cut at a word, and the full text starts the detail.

One more turned up while testing a thread's example (#V2, "Representing exponential numbers in OBX-5"): a unit marked UCUM in OBX-6.3, such as {copies}/mL, was reported as having no UCUM code because it was not in Perfuse's table of common spellings. It is now kept as a UCUM code, unless it cannot be one (a space outside braces, unpaired brackets).

Checked and found correct: subscription notification Bundles carry request and response on every entry, history entries' request.url is the interaction rather than the history URL, $log-questionnaire-errors pairs questionnaires and outcomes by position, DTR accepts referenced, and bulk export says requiresAccessToken: true.

Windows, on GitHub's Windows runners

Every push now runs the whole test suite on Windows Server (x64) and on Windows 11 on Arm, starts perfuse serve on a fresh database on both, and on x64 installs the service the way the manual says: perfuse init -service windows, then the generated install-service.ps1 as an administrator. The check is that the service runs as LocalService, answers, has written the one-time administrator password to its log, has shut ordinary users out of its folder, stops when asked, and is removed.

The first run failed in eleven packages. Most were the tests' own Unix assumptions (file modes, | in file names, paths pasted into JavaScript, CRLF checkouts), but five were in the product:

  • Two FHIR creates in quick succession could get the same id, and the second overwrote the first. The id fallback came

from the clock, and Windows' clock is coarse enough for two requests to read the same value. Ids are now random. The same flaw named files written by the S3, FTP, SFTP, document and CDA senders and the file poller; those now add random bytes.

  • The SMART signing key was refused when the server restarted, because Windows reports every file as readable by others.

Windows has no Unix modes; the key's privacy there is its folder's access list.

  • A refused or unreachable connection was reported as a bare error. The connection test now recognises Winsock's codes.
  • The service install left the data folder readable by every local user, because it inherited the drive's access list.

It now cuts inheritance and grants only SYSTEM, Administrators and the service account.

  • A service has no console, so its log and the one-time administrator password went nowhere - a fresh install nobody

could sign in to. Found by the service step's first run. serve -log-file writes both to a 0600 file, under the service manager it defaults to perfuse.log beside the database, and the generated script names logs\perfuse.log.

Not covered: a Windows desktop session, an installer, and a hospital's group policy. The xmlsec1 interoperability tests skip there, because the runner's Docker runs Windows containers and xmlsec1 comes in a Linux one.

The public test instance, test.perfuse.health

A Perfuse anyone can test against, with test data only: FHIR, CRD, DTR, PAS and SMART, on the released binary (checked against SHA256SUMS) or one built from main (below), with a Let's Encrypt certificate. It is built from the repository by deploy/test-instance, and a workflow checks every half hour that it answers and that its certificate has more than 14 days left. Certificate renewal was tested (certbot renew --dry-run). The data are the conformance runs' fixtures; Inferno's backend client may read everything and write only Claims and Subscriptions, and a write to anything else was checked to be refused.

Inferno's hosted test kits at inferno.healthit.gov, run against it on 11 October 2026 with v0.1.9 (conformance/inferno/hosted.py), gave the same results as the local runs (DTR and PAS run again on 15 October 2026 with v0.1.16: unchanged):

KitResult
Da Vinci CRD Server v2.2.192 pass, 43 skip, 8 omit
Da Vinci DTR Payer Server v2.2.043 pass, 1 omit, 2 fail (the same two must-support findings as locally)
Da Vinci PAS Server v2.2.182 pass, 2 fail (the same two must-support findings as locally)
SMART App Launch STU2.2, public app80 pass
SMART App Launch STU2.2, confidential app67 pass, 3 omit

The first DTR run found one difference, in the run rather than the server: the order it asks CRD about named a patient no member record matches, which the local payer never checks and the public one does. The order is now Amy Shaw's.

Since 19 October 2026 (v0.1.18) every screen carries invented data, so a visitor sees each one at work: deploy/test-instance/seed.py fills the FHIR store (30 patients with encounters, conditions, HbA1c results and blood pressures), the patient index (admissions from three systems, NHS numbers from the synthetic 999 range), the imaging archive (8 studies drawn by the script), users and machine tokens, data protection requests and a legal hold, a local code system, the Direct address book, a SMART Health Link, saved views and measures, and a TEFCA participant declaration; the demonstration channels add a patient index destination, an MLLP hop over TLS, publishing to the server's own XDS.b registry, a feed contract, a shadow comparison, a shared mapping table, and a billing system that goes away for ten minutes every three hours, so the queue fills and drains. The shared demo account may now read the administrators' screens. Every screen was then swept as that account with Playwright, as the live instance and as a local copy built the same way.

The sweep found five defects, fixed:

  • The console's channel list reported a feed contract beside its channel (the documented layout) as a channel that would not

load; the loader that runs channels already skipped it.

  • A shadow comparison started from the console named its candidate by file name alone, and the engine read that from the

working directory, so on a service (whose working directory is not the channels folder) every such comparison failed to start.

  • The XDS destination sent ${NAME} references in its token, username, password and headers as written.
  • The DICOMweb source and destination did the same, so a source configured as the manual shows (token: ${STOW_TOKEN})

accepted the literal text ${STOW_TOKEN} as its token. A reference to an unset variable now refuses every request.

  • A data protection request giving a record number on its own found the person's messages but not their patient index record,

so an erasure left the index record behind. A bare number now finds the one person holding it, and nobody when two do.

Also found: perfuse generate restarted its control ids and document numbers at 1 on every run, so a demonstration feeding it once a minute sent the same document number again and again (an XDS registry refuses a second document under one uniqueId). Without -seed the identifiers now start from the clock. Worse, a mixed batch was split by proportion, and three messages split that way are two ADTs and an MDM every time: the instance's lab and order channels had never been fed by its traffic timer. A batch under twenty is now drawn kind by kind, and an unseeded run starts at a random patient. And the manual's section on paths was wrong twice: it wrote a repetition as PID-3[2] (the parser takes PID-3(2)) and said a path without an index means the first repetition and the first occurrence, when it means every one (deliberately, as in filters).

The UK and Europe (v0.1.18, October 2026)

  • Translations (twelve languages): every string of the console is in each catalogue with its placeholders (a unit test),

and a Playwright sweep of every screen in every language fails on any catalogued text left in English, a page error or a horizontal overflow; all eleven pass. The manual's translations keep every code span and every list and table row (a unit test). Made by machine (Llama 3.3 70B through OpenRouter); not reviewed by a native speaker in any language.

  • National patient numbers: each of the 37 schemes' check digits is tested against valid numbers and one-digit

errors. The FHIR system URIs marked verified are taken from the national guides named beside them; about seventeen are defaults (marked unverified in the scheme list) for a site to confirm.

  • National code systems: SNOMED CT RF2, ClaML and the ICD-11 tabulation are read from fixtures written for the tests in the

published formats, not from a real national release (most need a licence); the WHO ICD API client was tested against the WHO's own ICD-API container (whoicd/icd-api 2.6.0, release 2025-01, English and French; PERFUSE_TEST_ICD11_URL): titles in both languages, residual codes (BA00.Z), unknown codes, and postcoordinated clusters, which the API checks whole (1A00&XS25 refused). That found that a cluster's display was its stem's title alone ("Fracture of neck of femur" for a right-sided one); it now names every part. The container answers as id.who.int does but takes no token, so the sign-in to the WHO's hosted API is still tested against a stand-in only. The module identifiers given for six editions (Belgium, Ireland, Norway, Switzerland, Estonia, Spain) are unverified.

  • GDPR tooling: find, export, erase with a second administrator's approval, holds, the erasure ledger and its reapplication

after a restore are tested on SQLite and PostgreSQL. The draft data processing agreement (docs/legal) has not been read by a lawyer.

  • Residency and hosting: the region lists are tested; the OpenTofu stacks pass tofu validate and tofu plan (eu-west-2:

55 resources, all in the Region). The London stack was applied in a real AWS account on 26 October 2026 and torn down. Perfuse v0.1.21 installed itself and ran on RDS over verified TLS with -residency uk. The console and FHIR answered through the load balancer, an HL7 v2 message over TLS on an MLLP port was acknowledged, and Session Manager, the backup job and the CloudWatch log all worked. The apply found two faults that plan cannot see: rule descriptions AWS refuses, and a health check that ignored the database and filled the log with failed handshakes. Both are fixed (deploy/hosted/README.md). The costs are AWS list prices.

  • National FHIR profiles (22 October 2026, on main): conformance/national/run.py converts NHS Wales' published v2

examples, the French agency's PAM-FR and document examples, and invented German messages, and the HL7 validator 6.10.4 checks every resource against UK Core STU2 2.0.2 (185 of 187 pass, re-run 24 October), FR Core 2.2.0 (138 of 156) and ISiK Basismodul 4.0.3 (12 of 37). The UK failures are a US race code in a Welsh sample and a terminology server error; the French ones Encounters whose messages carry no admission date or visit number; the German ones what ISiK requires that v2 does not say (contact level, the facility's IKNR, an MR-typed patient number, DG1-19). The runs found and fixed: German year-stamped code systems (I10G2026, OPS2016) written to a placeholder system; KVNR and NHS numbers left under their OIDs in the FHIR output; PI patient numbers without use: usual; practitioners' XCN-10 name type and XCN-13 identifier type dropped. Per-type results: conformance/national. Not a test of the FHIR server's ISiK search and capability requirements, and not gematik's conformance process (Bestätigungsverfahren).

  • HL7 Europe Laboratory Report 2.1.0 (23 October 2026, on main): the 23 lab result messages (ORU^R01) among those sets,

checked against the guide's DiagnosticReport, Observation, Specimen and ServiceRequest profiles and HL7 Europe Base 2.0.1 for the people and organisations: 77 of 391 pass (106 of 426 since the orders and interpreters were added, below, and order numbers keep the organisation that issued them). Every report fails on the guide's form (a DiagnosticReport must point at the Composition of an EU lab document, which a v2 result is not); most results on the data (287 name no performer, 260 have no time in OBX-14 or OBR-7; 25 French results are an embedded CDA report). Found and fixed: results without OBX-14 were not given their OBR-7; OBX-15 and OBX-23 to OBX-25, the producing laboratory and its director, were not read; a SNOMED CT version given as a bare date in CWE-7 was written as the coding's version.

  • HL7 Europe Hospital Discharge Report 1.0.0, ISiK with a site's own step, and Nictiz nl-core (24 October 2026, on

main). The 15 patient administration messages (ADT) among the sets, against the Discharge Report's profiles: 68 of 69 pass, the one failure the Welsh US race code. As with the lab report, the discharge report is a document a sender assembles from these. The run found PR1 not converted at all (now a Procedure, by HL7's v2-to-FHIR PR1 map, status unknown since an ADT does not say whether it was done) and DG1 missing from Encounter.diagnosis (now there, the admitting one as AD); a new invented German discharge supplies a stay with a procedure and an allergy. ISiK, with the 8 German messages: 18 of 47 on their own. What ISiK requires and v2 does not carry (IKNR, the contact level, an MR-typed patient number, the diagnosis type) a hospital adds with a FHIR destination's new bundle_script, JavaScript over each converted Bundle before it is checked and sent; with the example script (examples/isik/bundle-script.js, the hospital's own facts, not derived from the message) 42 of 47 pass. Left: two diagnoses without DG1-19, two practitioners without XCN-10, and one OPS coding the validator refuses because it reads Basis DE's value-set version * literally. The ISiK run also found that Procedure.category was written as a list, which R4 does not allow, and that OPS codes had no SNOMED CT category (now from the code's chapter, by HL7 Deutschland's OPS-SNOMED-Category map). Nictiz nl-core 0.12.1-beta.1 (zib 2020), with 3 invented Dutch messages: 12 of 24, 18 of 24 with the hospital's script saying which given names are first names and which initials (examples/nictiz/bundle-script.js). The other six are codes tx.fhir.org could not check against Dutch SNOMED CT reference sets. The Dutch run found family names sent in parts (FN subcomponents: van Voorbeeld&van&Voorbeeld) written whole into family; they are now the surname with its parts in HL7's family-name extensions, and Dutch v2's BSN issuer NLMINBIZA is known. Not a test of Nictiz's own qualification, of any HIS's real messages, or of the Dutch reference sets.

  • Who ordered a result and who signed it off (24 October 2026, on main; found answering a question on chat.fhir.org about

building the EU lab report from v2). OBR-16, the ordering provider, had been written as the DiagnosticReport's performer since the first conversion, which said the person who asked for a test produced its result; OBR-32, the principal result interpreter, was not read. Now each OBR of a result is also the ServiceRequest it answers (HL7's v2-to-FHIR ORU_R01 map), the report is basedOn it, OBR-16 is its requester and its status comes from ORC-5 or is unknown; OBR-32 is resultsInterpreter (as an NDL, or as the XCN many senders write there); OBR-34 and OBR-35 are performers with their function. Re-run: UK Core 185 of 187, FR Core 138 of 156 (the new practitioners and orders all pass), the EU lab report 105 of 426 (20 orders, 18 passing: two results carry no order number; 5 interpreters named by number only, which HL7 Europe Base refuses).

Bulk Data Access v2.0.0, the first run

The first run of Inferno's Bulk Data v2.0.0 Server suite gave 17 pass, 18 skip and 13 fail, and every failure but one was the server's:

  • A client could start an export and not cancel it. Cancelling is DELETE on the status URL, which the scope check

treated as a write; Bulk Data clients hold read scopes, so every cancel was refused, and the abandoned exports then blocked new ones. Cancelling now needs only read access, and answers 202, as the specification says (it answered 204).

  • The CapabilityStatement listed no system or Patient export, though both were served.
  • The manifest was sent as application/fhir+json. It is not a FHIR resource; it is now application/json.
  • An R4 server exported R5. Export files held the internal shape, so Encounters had R5's list of classes and status

completed. Each exported resource now goes through the same version conversion as a read.

  • One fixture was ours: the approved authorization had an addItem without the adjudication R4 requires.

Now 46 pass and 2 fail. Both failures are one resource, Inferno's own adaptive DTR questionnaire, whose artifact-versionAlgorithm extension uses a code system R4's validator does not know.

That questionnaire turned out to be ours: the DTR run adds the extension to Inferno's fixture, in its Coding form. Written as the string its definition also allows, the suite passes 48 of 48.

UM systems take 278s over CAQH CORE's connectivity (SOAP or HTTP MIME), or as batch files. Each transport was written from CAQH's Connectivity Rules vC2.2.0 and vC4.0.0, their XML Schema and WSDL (CORERule2.2.0.xsd and .wsdl, downloaded from CAQH; the vC4.0.0 schema as the rule prints it), and the CMS esMD 278 companion guide, then tested against a stand-in UM system that does not use Perfuse's code:

  • written in Node, reading CAQH's WSDL with node-soap 1.14.0 and validating every CORE envelope it receives, and every one it

sends back, with libxml2's xmllint against CAQH's schema;

  • checking the CORE metadata as the rule's error table has it (payload type, processing mode, a UUID payload ID, a time stamp

with its zone, receiver ID, rule version), the WS-Security UsernameToken, a client certificate from its own test CA and OAuth client-credentials tokens, answering faults and envelope errors (UnAuthorized, VersionMismatch) as the rule says;

  • judging each 278 with the outside checker of the section below, answering one it fails with a 999 rejecting it, and the

rest with a 278 response in the shape of the esMD samples (certified with a number, pended with reason 0U);

  • in batch, behind OpenSSH's own sshd and sftp-server: it answers each 278 file with a 999 and a 278 response, and

records the 999 Perfuse sends back.

TestThePASExamplesAreDecidedByAnOutsideUMSystem sends the PAS 2.2.1 examples over X12 with a password and with OAuth, CORE SOAP vC2.2.0, CORE SOAP vC4.0.0 with MTOM, a client certificate and OAuth, CORE HTTP MIME, and SFTP: every one decided as the stand-in decided it, none of its envelopes failing CAQH's schema. A wrong password is refused by the envelope, vC4.0.0 without a client certificate at the door, and a broken 278 comes back as a 999 that reads as a rejection. A server configured from the settings screen alone (no flags, the password saved there) reached the stand-in after a restart.

Found and fixed on the way:

  • Interchange control numbers repeated. They were the clock's seconds, so two requests in one second went out with the

same ISA13, which a payer discards as a resend. Each is now one more than the last, kept with what became of it.

  • A TA1 or 999 instead of an answer was "not a 278 response". It is now read: the items pended with what it said, and the

exchange shown on the Reviewer queue screen.

  • The TA1 Perfuse writes for mismatched ISA and IEA control numbers said 022 ("invalid control structure"); the code for

that is 001.

  • A text flag holding digits stopped the server. -pas-278-payer-id 12345 was read as a number and the settings file

refused it at start.

  • Settings that name a flag were not all applied to it. The table linking them was kept by hand and had fallen behind;

-index-identity was among the missing, so its console switch was saved and then ignored at start. It is now built from the settings themselves, and a test holds the two together.

$inquire through the UM system (23 October 2026). A PAS inquiry is also sent to the UM system as a 278 inquiry (005010X215), so authorizations asked for by fax, portal, phone or another submitter's 278 are found; one of PAS's authors confirmed on chat.fhir.org that $inquire should return them. The stand-in was given a checker for the inquiry written from the 278 inquiry companion guides UnitedHealthcare, Cigna and Blue Cross Blue Shield of Massachusetts publish (BHT 0007/28, the UMO with a payer ID, the requester's NPI, the member's ID with a name or date of birth, one patient event with UM01 and a number or the event dates, SE's count), and answers with everything it holds for the member, an authorization it was given by fax among them, in the shape of Blue Cross Blue Shield of Massachusetts' sample response (BHT02 49). Over X12, CORE SOAP vC2.2.0 and vC4.0.0 and CORE HTTP MIME (payload type X12_278_Request_005010X215), the inquiry passed the checker and CAQH's schema, the fax's authorization came back once, and none of the requests Perfuse had forwarded came back a second time. The answers validate against PAS 2.2.1's inquiry response profiles (the HL7 validator, 6.10.4) except for what the PAS example's own Patient carries. Not tried against a real UM system.

Not tested: a real payer or clearinghouse (that needs a trading partner agreement and a test account); CAQH CORE certification, which only CAQH's authorised testing vendors do; CORE's batch SOAP operations and the vC4.0.0 REST rule (whose message body the rule gives only as an informative example); 275 attachments.

To run it: npm install and node standin.mjs in the stand-in's folder (with SFTP_ROOT for batch, an sshd serving that folder), then PERFUSE_TEST_UM_STANDIN=https://127.0.0.1:18278 PERFUSE_TEST_UM_STANDIN_TLS=<its tls folder> and, for SFTP, PERFUSE_TEST_UM_STANDIN_SFTP*, and go test ./internal/fhirserver -run OutsideUMSystem. The stand-in is kept outside the repository, beside the checker.

PAS to X12 278, against published 278s (20 October 2026)

-pas-278-url sends each PAS request to a payer's utilization management system as a 278 and reads its 278 answer back. Until now only Perfuse's own tests stood behind it: they wrote both the 278s and the stand-in that answered them, so they could only agree with themselves. Three outside sources were used instead:

  • The PAS 2.2.1 guide's own request examples (homecare, homecare update, medical services, referral, surgical admission),

sent to Claim/$submit with a stand-in UM system recording the 278 it received (TestThePASExamplesBecome278sThatSayTheSame).

  • The CMS esMD X12N 278 Companion Guide's samples: 31 transactions printed in the guide, of which the eight in

internal/x12/testdata/esmd are read by the tests (TestTheCMSPublished278sAreReadAsTheySay).

  • A 278 checker written from public sources, not from Perfuse's code: the esMD guide, UnitedHealthcare's and Utah

Medicaid's 278 companion guides, the TR3's example transactions as OopFactory's tests carry them, and OopFactory's 278 loop structure. It checks envelopes and counts, the HL hierarchy, loop and segment order, and the code values those guides publish. Before it judged Perfuse it was run over the published samples, and its findings there were the samples' own faults (count and control-number mismatches the guide's text carries). PERFUSE_TEST_X278_CHECK runs it over the 278s the PAS examples become.

What was found, all fixed:

  • Urgent was sent as an emergency. UM06 (level of service, X12 1338) is E elective, U urgent, 03 emergency. Perfuse wrote

03 for an urgent request, and read only 03 as urgent, so CMS's urgent sample read as routine.

  • A cancellation was "unknown". HCR01 C, which the esMD samples send for a cancelled review, was not recognised, so the

provider was told an unknown code had pended the request. It is now a cancellation (PAS review action C).

  • An admission review read as a health services review. A 278 service line carries its own UM01 (HS), and it overwrote

the event's AR. An elective admission was also guessed urgent because it was an admission; UM06 E now says otherwise.

  • The request left out what PAS carries and payers ask for. Only the certification type was taken from PAS's extensions.

Now also: the review type (UM01, so a referral goes as SC and an admission as AR), the level of service (UM06), the place of service (UM04, which esMD and UnitedHealthcare require), an admission's admission and discharge dates (DTP 435 and 096), the care team's provider who will do the service (NM1 SJ), and an institutional claim's service as SV2 rather than SV1. Segment order follows the TR3 (DTP before HI).

All five PAS examples now become 278s the checker passes with no warnings. Not tested: a real UM system or clearinghouse, X12's own TR3 (it is sold, and its situational rules are not all public), the transports UM systems use (CAQH CORE SOAP and MIME, SFTP batch) and 999/TA1 acknowledgements.

A PAS decision service, end to end

serve -pas -pas-decider-url was run against a decision service (Vaglio, an optional BioDream decision engine; Perfuse needs none) on this machine, with Perfuse's rules leaving lumbar MRI to a reviewer. Four synthetic PAS requests went to Claim/$submit: one approved and one denied on the service's policy, each with its reason in the process note, one pended naming the information it lacked, and one pended because its record contradicted itself. Each call carried X-PAS-Request-Id. A reviewer then approved the pended case in the service, which sent Claim/$decide under that id, and Perfuse's ClaimResponse for the request read A1 (certified in total). The failures - the service down, an error, a timeout, a denial with no reason, an unknown decision - are covered by cmd/perfuse/decider_test.go, and each pends the item.

Da Vinci PDex 2.0.0 Payer Server

The Payer-to-Payer workflow passes 19 of 19, and the API and must-support coverage 312, with 1 skip. The data are Inferno's public reference patients 85 and 355 (synthetic), plus PDex prior authorizations, a Device, MedicationDispense and Provenance built from the PDex package's examples, each validated against its profile first.

The first runs found five server faults:

  • $everything on an R4 server returned R5 Encounters, skipping the per-entry conversion a search applies.
  • $member-match was refused for read scopes. It finds a member and stores nothing.
  • Its response carried MemberId, which HRex 1.2.0 allows and HRex 1.0.0 (PDex 2.0.0's) refuses. It now sends the

identifier alone, valid under both.

  • An R4 client's Encounter?status=finished found nothing, because the index holds R5's completed.
  • ExplanationOfBenefit?service-date read only the billable period; R4 defines it as billablePeriod | item.serviced.

The skip is the prior authorization's must-support list, which no valid resource can cover in PDex 2.0.0. The PAS item extensions it names resolve to PAS 2.0.1, where their context is Claim.item and ClaimResponse.item only. And the allowedunits, denialreason and consumedunits slices fix categories from PDexAdjudicationDiscriminator, which the required PDexAdjudicationCategoryDiscriminator value set does not include. PDex 2.2.0 with PAS 2.2.1 fixes both, so this is for the kit to move to 2.2.0 (reported on davinci-pdex-test-kit issue 63), not for the server.

Da Vinci Plan-Net 1.1.0 Server

The provider directory was new: Plan-Net's search parameters and includes, _has, and its profiles in the CapabilityStatement. The data are the Plan-Net package's own examples. The suite now gives 199 pass, 44 skip and 10 fail. Building it found three faults in searches that already existed:

  • A path into an extension never indexed, because the URL's dots split the path. Condition's asserted-date had

never worked.

  • A chain whose inner condition matched several resources found nothing: the matches were ANDed.
  • _id=a,b looked for an id spelled "a,b", and _id=a&_id=b found either.

The 10 failures are forward chains through partof, network and a service's organization, where the kit takes its value from the first Organization it has seen rather than one the searched resources refer to; the network case is reported upstream (davinci-plan-net-test-kit issue 34).

AS2, EDIFACT, mail and the patient index, against pyas2lib, pydifact, GreenMail and FEBRL

Repeatable with sh conformance/interop/run.sh, which builds each reference from a pinned version (conformance/interop).

  • AS2, against pyas2lib 1.4.3, both ways. Triple DES (pyas2lib's default) and AES-256, signed and

encrypted, compressed (RFC 5402) and not, signed synchronous receipts whose MIC each side checks. Writing the compressed case found that the RFC's MIC rule differs between signed and unsigned messages; both are followed. A fuzzer on the new BER reader (Java gateways write CMS with indefinite lengths) found an out-of-range read on a truncated OCTET STRING segment within seconds; fixed before it was committed.

  • A resend (RFC 4130 section 7.5). The same message posted three times, once compressed, and ten copies

at once: passed on once each time, every copy answered processed, and still recognised after a restart. Removing the check makes the test fail ("passed on 3 times").

  • UN/EDIFACT, against pydifact 0.1.8, both ways. What Perfuse writes pydifact reads segment for segment,

and the reverse, release characters and UNA included.

  • IMAP, POP3 and SMTP, against GreenMail 2.1.3. The POP3 test failed on mail left behind by an earlier

run; it now empties the mailbox first.

  • The patient index, on FEBRL dataset 3 (5,000 synthetic records of 2,000 people). No record linked to

the wrong person with automatic linking off (the default), on with the published weights, or on with weights the index estimated from the records. Measuring found that one blocking key per record let 902 duplicates (30%) go unseen: a mistyped birth year, or no birth date, hid the person. Two more keys (both names in either order; postal code and given name) brought that to 351, and every one of those left differs on nearly every field. The estimate itself first came out wrong (chance agreement on the birth date put at 45%), because the pairs it learned from had been chosen for agreeing on names and dates; chance agreement now comes from random pairs, as Splink and fastLink do it.

Clusters on AWS (2026-10-18)

Two Linux servers (t4g.medium, ARM64) and one Windows Server 2022 (t3.large) in three availability zones of us-east-1, sharing Amazon RDS for PostgreSQL 16 (db.t4g.large) over TLS with sslmode=verify-full against the RDS CA bundle. Each server served its console over HTTPS with its own certificate. An internal Network Load Balancer, one target group per feed with HTTPS health checks on /readyz?channel=NAME, carried the feeds; a sender on a fourth instance resent each message until answered with AA, and a receiver counted what arrived.

  • Active-passive: the server running the feed killed with SIGKILL / Stop-Process -Force twice, the receiver refusing in

between so acknowledged messages waited in the shared queue: 2,000 acknowledged, 0 lost, 0 duplicates.

  • A leader cut off from the database by a firewall rule (Linux iptables; Windows outbound block) with its process left

running: it stopped its feed 7-10 s after its last renewal; another started it 6-8 s after that. 3,000 + 3,000 messages, 0 lost, 1 duplicate.

  • Clocks: one server 120 s fast, one 90 s slow, leader killed: takeover after about a lease; 2,000 messages, 0 lost.
  • Active-active, four feeds on three servers, one killed and restarted: 6,000 messages, 0 lost, 2 duplicates.

Found and fixed on the way:

  • Every server's first start wrote its own certificate path into the settings file every server shares, so the Windows

server tried to open /home/.../node1.crt. Per-server settings (TLS certificate and key, SNMP address, trace name) are now kept out of the shared file and refused there.

  • A server that came back after a crash stayed idle beside two holding two channels each (4 over 3 rounded up is 2).

Servers now give one back when two or more ahead of the least busy, and leave it a few leases rather than retaking it.

  • With cross-zone load balancing off, the AWS NLB node in a zone with no healthy target failed open and a sender waited

95 s. Documented: turn it on.

The full suite with the race detector also ran on Linux x86-64 (c7i.2xlarge), on SQLite and on PostgreSQL 16.4. Two tests were wrong for that machine and were fixed: the message-store speed test compared against a bare insert that on Linux never waits for the disk (now an absolute floor there, and not judged under the race detector), and the GnuPG test failed where gpg has no agent (now skipped there).

PIX and PDQ v3, and Direct over IMAP, XDR and LDAP (October 2026, on main)

  • PIX and PDQ in HL7 v3 (/pixv3, /pdqv3). IHE's own sample messages (IHE/ITI-Info, examples/PIXV3 and PDQV3, CC BY

4.0, kept in internal/pixv3/testdata/ihe) are fed and asked in IHE's order: two records added, one revised, the PIX query answered with the cross-references the published response shows (the second domain's number and the social security number), duplicates resolved into one person, and the PDQ query paged two at a time, continued and cancelled. Every answer Perfuse writes (MCCI_IN000002UV01, PRPA_IN201310UV02, PRPA_IN201306UV02) validates against IHE's HL7 V3 schemas (NE2008 multicacheschemas, PERFUSE_HL7V3_SCHEMA), the refusals included: an unknown patient, an unknown domain, a merge naming no prior record, a query naming no demographics. Over SOAP with API tokens, a viewer may query and not feed, and a record fed in v3 is the one PIXm answers over FHIR. Not checked against an independent PIX or PDQ v3 implementation (Gazelle's simulators, IPF, a vendor's MPI): the schemas check the shape of every answer and IHE's samples their content, but no other system has read one.

  • LDAP certificate discovery, against OpenLDAP 2.4.57 (osixia/openldap 1.5.0, PERFUSE_TEST_OPENLDAP): an address-bound

certificate in userSMIMECertificate and a domain-bound one in userCertificate;binary, both written by OpenLDAP's own ldapadd, are found through the root DSE's naming contexts with an anonymous bind; an address with no entry gets the domain's; DNS first, then the directory, and off asks none. The SRV lookup for a domain's directory is tested only for a domain that has none.

  • A Direct mailbox read over IMAP, against GreenMail 2.1.3: a message Perfuse sealed for the address is read, opened and

passed on, and a plain message that is not Direct moves to the error folder.

  • XDR. An xds destination with direct_from and direct_to posts to a direct source's XDR endpoint, which passes the

CDA document on and refuses a submission addressed to someone else with a RegistryResponse failure. The NIST XDS Toolkit's Document Recipient simulator (7.13.0, PERFUSE_NIST_REC) accepted the XDR after one fix it found: the xds destination sent a CDA document's confidentiality as the CDA reader's meaning (normal) rather than its code (N), and a coded attribute without a display had no Name, which the recipient's metadata validator refuses (XDSRegistryMetadataError, "no name attribute"). Perfuse's own registry, and the toolkit's Registry/Repository with validation as configured earlier, had stored both. The code is now sent, with its meaning as the display, and a code without a display is named by itself.

  • XDR against ONC SITE's Direct XDR Edge Test Tool (site.healthit.gov/direct/xdr-edge, 20 October 2026), the reference the

(h)(2) certification test uses, both ways: - *Perfuse sending:* SITE's XDR validator (PERFUSE_SITE_XDR) at first answered every message with a SOAP fault, "Local name may not be null or empty". The message was legal XML: encoding/xml writes each metadata element with its namespace redeclared as the default, and SITE's parser (Apache Axiom) cannot read that. Perfuse now writes XDS metadata with each namespace bound once to its usual prefix (rim:, lcm:, xdsb:), as IHE's own samples do. SITE then reported one error from its Direct XDR checklist, wsa:MessageID without mustUnderstand (XDR_MSG_426), now added. It now answers Success with Info notes only, about optional elements Perfuse leaves out (wsa:From, wsa:FaultTo, direct:metadata-level, the request's id and comment). The NIST Document Recipient and Registry/Repository still accept the new form. - *SITE sending to Perfuse:* both of SITE's message types (minimal and full metadata) with an ONC C-CDA sample were accepted by a direct source's XDR endpoint on test.perfuse.health, the document kept byte for byte, and a submission addressed to another Direct address was refused with a RegistryResponse failure naming both addresses. SITE's sender writes plain HTTP to a raw socket (even for an https URL) and reads until the connection closes, so for this test only a socat relay on a temporary public port forwarded to an XDR endpoint on loopback, closing after 5 idle seconds; the channel, relay and port were removed afterwards. - Not tested: a commercial HISP's XDR endpoint.

  • ONC's Direct Certificate Discovery Tool (DCDT, site.healthit.gov/direct/dcdt, dcdt31.healthit.gov, 20 October 2026). Its

18 discovery cases each name an address whose certificates the tool publishes in DNS CERT records or in LDAP directories named by SRV records: some valid, some expired, revoked, bound to the wrong name, or only verifiable through an intermediate fetched by AIA, with directories that are down or ranked by SRV priority and weight. The tool's own verdict arrives by mail after a message is sent to the address; TestTheDCDTDiscoveryCases (PERFUSE_TEST_DCDT) asks the same question without sending, running a Direct destination's certificate selection with the tool's root as the only anchor. At first 9 of 18 passed, and the 9 failures were four defects, all fixed: - the SRV lookup for a domain's directory used the system resolver, not the dns server the destination names; - a SERVFAIL answer for the address-bound name (which the tool's DNS gives for a name holding nothing) ended the lookup before the domain-bound name was asked; - Perfuse sent LDAP searches with a 10-second server time limit, and the tool's ApacheDS answers any time limit with timeLimitExceeded and no entries (OpenLDAP had not minded); - certificates were never checked against their issuer's CRL (D17 has two revoked certificates beside the valid one), and a missing intermediate was never fetched from the AIA caIssuers URL (D18). All 18 now pass. A revocation list that cannot be fetched still lets delivery go ahead (a CRL server that is down must not stop all Direct mail); a list not signed by the certificate's issuer is ignored. Unit tests cover each with local stand-ins.

  • A test domain under the tool's hosting cases. On a temporary EC2 instance, direct.perfuse.health was delegated to

CoreDNS publishing address- and domain-bound CERT records and an SRV record for an OpenLDAP 2.4 directory holding address- and domain-bound certificates. The DCDT's hosting tests H1 to H4 passed for it, and Perfuse's lookup found the same four certificates the tool did. (Perfuse does not publish certificates; this checks the test rig and gives a second, independent discovery to compare with.)

  • SITE's HISP, Apache James, over IMAP. A Direct source read a sealed message from a mailbox on james.healthit.gov (the

account SITE publishes in its "ETT Endpoints" guide), over STARTTLS with James's self-signed certificate trusted through the new imap.tls.ca_file and server_name, and moved a message that was not Direct to the error folder (PERFUSE_TEST_SITE_JAMES; the test deletes its folders). A Direct source reads IMAP only, and James's POP3 offers no STARTTLS, so POP3 was not tried.

  • Not tested: SMTP with SITE. After our trust anchor was uploaded to SITE's Transport Test Tool, its sender reported

three messages "sent to the host successfully" to an address on the test domain, and looked up the address's CERT record each time, but no connection ever reached the test domain's MX on port 25. Sending the other way needs outbound port 25, which AWS holds for new accounts until a request to lift it is granted. SITE's HISP Testing Portal (SMTP, IMAP and POP3 as sender and receiver) needs a SITE account.

The German hospital standard (ISiK), against gematik's own test suite (October 2026)

gematik publishes the ISiK test suite (github.com/gematik/isik-testsuite, release 3.0.9, Stufe 5) and runs it in its own container. conformance/inferno/isik points it at Perfuse's ordinary FHIR server, with the records each test expects in conformance/inferno/isik/data (invented, no real person: drafted from the suite's own preconditions and checks, then corrected where a check said the record was wrong).

ModuleFirst runNow
Basis (patients, encounters, diagnoses, coverage, accounts, allergies, observations, procedures, locations, practitioners, value sets, documents)179 of 421421 of 421

What it found in Perfuse, all fixed:

  • No XML. Most of the suite's requests ask for FHIR XML, which Perfuse did not write. It now answers in XML when asked

(Accept: application/fhir+xml or _format=xml), with the element order taken from the R4 and R5 core definitions; checked on 295 of the R4 core examples with the HL7 validator, which found no structural error from the conversion. Bodies sent in XML are refused with 415: Perfuse reads JSON only.

  • No charset on application/fhir+json answers, which ISiK requires.
  • About forty search parameters ISiK requires, among them _tag on every type, an Encounter's date-start, end-date,

account and service-provider, an Observation's combo-code-value-quantity, a ValueSet's context-type-value, a Location's near (by distance), and Coverage, Account and RelatedPerson parameters. The capability statement now also lists _count and _has, and gives url as uri.

  • Modifiers and forms clients use: :not on token parameters and _id, param:identifier= (resolved as a chain),

chains through Coverage and Encounter references, ?_count with no value, /Patient/?... with a trailing slash, and status=system|code on code elements, which carry no system in the data.

  • A Binary read as itself: with Accept: text/plain (any type but a FHIR one) a Binary answers with its content.
  • A document posted to the base URL is kept, and refused (422) when its Composition has no narrative or names a patient

or encounter this server does not hold by identifier.

Not run yet: the suite's other modules (documents, medication, appointments, vital signs, lab, intensive care, Connect), which need their own records. Not shown: a hospital's real systems feeding Perfuse.

Post-acute care (PACIO), against PACIO's own test kits (October 2026)

PACIO publishes Inferno kits for its guides (github.com/paciowg). Perfuse's ordinary FHIR server was loaded with PACIO's own data, the two public servers its Connectathon track uses and the examples in each guide, and run against all four (conformance/inferno, suite pacio):

GuideKitResult
Transitions of Care 1.0.0pacio-toc-test-kit 6141da024 pass, 4 fail, 8 skip
Standardized Medication Profile 1.0.0pacio-smp-test-kit 03892d534 pass, 3 fail, 14 skip
Advance Directive Interoperability 2.0.0-ballotpacio-test-kit 8bac2a774 pass, 23 fail, 4 error, 3 skip
Personal Functioning and Engagement 2.0.0pacio-test-kit 8bac2a740 pass, 16 fail, 1 error, 1 skip

What it found in Perfuse, all fixed:

  • A stored document was refused. The FHIR store would not keep any Bundle, so a transfer summary, a patient summary or

a lab report document could not be PUT; and reading a Bundle as a resource failed on its first entry. Documents, collections, messages and transactions kept as records are now stored, read back whole and found by type, identifier, timestamp and composition. A transaction PUT is kept, never applied; search sets and histories are refused.

  • A stored document read back as R4 held R5 resources. The Bundle was converted, the resources inside it were not

(an Encounter's class as a list, participant.actor). Every entry is now converted, and what the Bundle struct does not model (signature, language) is kept.

  • Searches that answered 400: Patient by active, telecom, address, address-city, -state, -postalcode; List by

code and patient; DocumentReference by custodian.

  • No way to say which guides the server serves. -fhir-declares (Settings, FHIR, Capability statement) adds guides to

instantiates and profiles to each type's supportedProfile. A statement, not a check.

What is left is not Perfuse's:

  • PACIO's sample data. Most failures are its examples: identifiers on example.org (which the validator refuses outside

examples), codes the validator's terminology does not know, a TOC document whose Composition carries no category the kit looks for, a CarePlan without its required category slice. One sandbox Patient (patient-1) claims US Core and has no identifier, which US Core requires; Perfuse refuses it, as it should, so the read of it fails.

  • Must-support elements the data never fills (note, abatementDateTime, target.dueDate, orderDetail ...): those tests

ask for a resource holding each element, and none of PACIO's does. Perfuse returned every element it was given: all 1,192 loaded resources were read back and compared.

  • The kits themselves. The general kit reads with the same credentials for its "unauthorised request" check, so a server

that grants them answers 200; its validator has no copy of the ADI ballot package (the error results); it names four ADI profiles the published 2.0.0-ballot does not contain; its .env.production points FHIRPath at localhost inside its own containers (worked round in kit.sh).

What this does not show: a transfer between two real systems.

A hospital's C-CDA as a TOC document (October 2026)

write: transfer (and perfuse fhir transfer) turns a C-CDA into a TOC document Bundle. Checked with the official FHIR validator (validator_cli, -ig hl7.fhir.us.pacio-toc#1.0.0 -profile .../TOC-Bundle) on three of HL7's own C-CDA examples (C-CDA-Examples, CC0): the Transfer Summary, the Discharge Summary and CCD 2 (internal/cda/testdata/hl7; PERFUSE_TEST_FHIR_VALIDATOR=validator_cli.jar go test ./internal/cda -run Validator). No errors but one, the guide's: each vital sign matches both its simple-observation and vital-signs Bundle entry slices (16 of them across the three), which no sender can avoid. The guide's own example Bundle fails the same validator on 33 counts of its own.

Found on the way, in the conversion every C-CDA already went through, all fixed:

  • Every allergy was coded as its type, not its substance. The allergen is the observation's participant; the conversion

read the value ("allergy to substance", 419199007), so every AllergyIntolerance named no substance.

  • A value's code system was the observation code's. An allergy reaction or a problem coded in SNOMED CT went out as an HL7

act code (the system of ASSERTION); results with coded values likewise.

  • R4 output carried R5 members. medication.concept, a Procedure's occurrenceDateTime and status recorded went into R4

bundles, which an R4 server refuses. A Procedure read through the generic path kept occurrenceDateTime as well.

  • A refuted problem was given clinicalStatus refuted, which is a verification status.
  • Vital signs failed FHIR's own profiles: systolic and diastolic sent apart instead of one panel, no magic code beside a

specific one, kg/m2 without UCUM, the category display vital-signs instead of Vital Signs.

  • Discharge medications never converted: each sits inside a discharge-medication act.
  • "No known allergies" was refused as a negated allergy, though FHIR has a code for it.
  • Document types were shifted by one: a Transfer Summary (22.1.13) read as a Procedure Note, a History and Physical as

Unstructured. The table is now checked against HL7's examples.

  • R5's attachment.size was a number; R5 makes it an integer64, a JSON string.

Not done: the text-only sections' entries (functional status, mental status, plan of care, devices, advance directives) are not turned into coded resources, and the guide's TOC DocumentReference index is not built. Not tried: a real receiving system.

What has not been verified

Stated plainly, because an unverified claim that nobody writes down becomes a claim everybody assumes was checked.

  • Windows has been tested only on GitHub's runners (above): no desktop session, installer or hospital group policy.
  • The Linux arm64 binary has run only in the cluster tests on AWS Graviton (above). The amd64 binary was verified inside

Alpine, which has no glibc, confirming the static-linking claim.

  • OpenPGP against GnuPG has run on Linux and macOS only. GitHub's Windows runners carry Git's MSYS gpg, which cannot

start an agent for native paths; Perfuse's own OpenPGP tests run there. An OpenPGP test that failed now and then in CI (decryption with session key failed: parsing error, twice on Windows arm64, then on Linux) was a real defect: the message and key the test printed decrypted perfectly when replayed, and the message ended in 0x1C, which the HTTP source took for an MLLP end byte and cut off. It now removes MLLP framing only as a whole frame, and a test seals until a message ends in 0x1C and posts it.

  • Clusters have run only in the tests above: three servers, one region, one load balancer type (AWS NLB). Not

across regions, not behind an F5 or HAProxy, not with PostgreSQL failing over to a standby of its own.

  • No container image has been published, though the build file and the README both name one.
  • No utilization management system or clearinghouse has received a 278 from here. The 278s were checked against PAS's

examples, CMS's esMD samples and an outside checker, and every transport against a stand-in written from CAQH's rules (above). Perfuse is not CAQH CORE certified.

  • No payer or clearinghouse has received a 275 built here, and it was built from published companion guides rather than the X12 technical report. See above.
  • The Inferno US Core SMART groups have not been run as US Core groups. The SMART App Launch STU2.2 suite they are built on

has, against Perfuse's own authorization server (below); the US Core kit was not installed, so its granular-scope groups were not.

  • No payer has verified a DSDR signature made here. The EU DSS validator has, as above, with a test CA.
  • No public health agency, or AIMS, has received a case report from here. The eICR and the eCR message validate, and the trigger

codes were the built-in sample, not the RCTC. No state has received an ELR message from here; see the ELR section for what the NIST validator does and does not show.

  • No third-party FHIR subscriber has received a notification from here. Delivery was tested against receivers written for the tests.
  • No translation has been reviewed by a native speaker, and the hosted-region stacks have never been applied (above).
  • No site has run production clinical traffic through any of this. The engine, transports, queue and

web interface are tested, and the parts that talk to other software are verified as described above. That is not the same as having survived a real hospital's Monday morning.

Shadow mode exists precisely so that nobody has to take this document's word for any of it: run Perfuse beside whatever is in place today, on real messages, delivering nothing, and read the differences.