contract

package
v0.1.23 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package contract turns what a feed looks like into what a feed must look like.

A profile answers "what am I receiving". That is useful once. The expensive problem is the day it changes: a vendor upgrade drops a field, a new code appears, a segment starts repeating. None of those is an error - the messages are still valid HL7 and every engine in the world accepts them - so nothing notices until a receiver falls over, usually weeks later, and the investigation starts from the wrong end.

A contract is the profile written down as an expectation and then checked. It is contract testing, which is a solved idea in software generally and almost entirely absent from healthcare integration.

Why the unit is a rate and not a message

The obvious design is to check each message and complain when one fails. That produces a system nobody can use. Real feeds contain a proportion of genuinely odd messages - a test message from the vendor, a patient with no recorded sex, a manual entry - and alerting on each one trains people to ignore the alerts, at which point the feature is worse than nothing because it has consumed the attention it needed.

So an expectation is about a proportion over a window: "PID-3 is populated in at least 99% of messages". One odd message never fires. Five percent of messages losing their medical record number does.

A contract describes what arrives, not what leaves

This is the distinction people get wrong first, including me while integration-testing it. Perfuse stores the message as it arrived - that is deliberate, because the stored message is evidence of what a sender sent - so a contract is checked against the incoming feed and not against the output of the channel's transformations.

That is the right default by a wide margin: the thing that changes without warning is the sending system, and the point of a contract is to notice. But it means an expectation written about a mapped value ("after mapping, only the lab's codes should remain") will fail immediately and confusingly, because the codes it names never appear in the traffic being profiled. Anything about what leaves belongs in a channel test, which runs the real transformations.

Why a violation names the tolerance it broke

"PID-3 failed" tells somebody nothing. "PID-3 was populated in 94.2% of the last 500 messages; the contract requires 99%" tells them how bad it is, how confident to be, and whether it is worth waking somebody. The numbers are the message.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Contract

type Contract struct {
	// Expectations are checked independently. Order does not affect the result but is preserved, because a
	// file somebody wrote should read back the way they wrote it.
	Expectations []Expectation `yaml:"expectations" json:"expectations"`

	// MinMessages is how many messages must be seen before any expectation is judged.
	//
	// Without this a contract fires on the first message of the morning, when one absent field is 100% of the
	// evidence. Defaults to 100, which is enough for a 99% threshold to mean something.
	MinMessages int `yaml:"min_messages,omitempty" json:"minMessages,omitempty"`

	// DerivedFrom records the profile this came from, when it was promoted rather than hand-written.
	DerivedFrom string `yaml:"derived_from,omitempty" json:"derivedFrom,omitempty"`
}

Contract is a set of expectations about one feed.

func Promote

func Promote(p *profile.Report, opts PromoteOptions) *Contract

Promote builds a contract from an observed profile.

func (*Contract) Validate

func (c *Contract) Validate() []error

Validate checks the whole contract.

type Expectation

type Expectation struct {
	// Path is the field or segment, in the same notation filters and transformations use, so an expectation
	// can be pasted into a filter and vice versa.
	Path string `yaml:"path" json:"path"`

	// Rule is what is expected.
	Rule Rule `yaml:"rule" json:"rule"`

	// MinRate is the proportion of messages that must satisfy the rule, between 0 and 1.
	//
	// Defaults to 0.99 rather than 1.0, deliberately. An expectation of "always" is almost never what
	// somebody means: real feeds contain test messages and manual entries, and a contract that fires on one
	// of them in ten thousand is a contract that gets switched off.
	MinRate float64 `yaml:"min_rate,omitempty" json:"minRate,omitempty"`

	// Values is the permitted set for OneOf.
	Values []string `yaml:"values,omitempty" json:"values,omitempty"`

	// Limit is the maximum for MaxRepeats.
	Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`

	// Why records who decided this and when.
	//
	// This is the domain-tax field. Most of what makes an interface hard to maintain is that nobody knows
	// why a decision was made, and the person who knew has left. An expectation without a reason becomes an
	// expectation nobody dares change and nobody dares delete, which is how a contract turns into
	// superstition.
	Why string `yaml:"why,omitempty" json:"why,omitempty"`

	// Relaxed records that this expectation was deliberately loosened, and why.
	//
	// Separate from simply editing MinRate, because an assertion silently weakened is worse than one that
	// was never written: it still looks like a guarantee. Recording the relaxation keeps the history visible
	// in the file that carries it.
	Relaxed string `yaml:"relaxed,omitempty" json:"relaxed,omitempty"`
}

Expectation is one thing that must hold.

func (Expectation) Validate

func (e Expectation) Validate() error

Validate checks an expectation makes sense before it is ever used.

type PromoteOptions

type PromoteOptions struct {
	// AlwaysThreshold is the fill rate above which a field is treated as always populated. Defaults to 0.995.
	AlwaysThreshold float64

	// MinRate is the tolerance written into generated Populated expectations. Defaults to 0.99.
	//
	// Lower than what was observed on purpose. An expectation generated at exactly the observed rate fires on
	// the first message that is slightly worse, which is the same day it was created.
	MinRate float64

	// MaxCodeSetSize is the largest value set that will be promoted. Defaults to 12.
	//
	// Small on purpose. A field with thirty distinct values might be a code set or might be free text that
	// happens to repeat, and getting that wrong asserts patient data.
	MaxCodeSetSize int

	// MaxCodeLength is the longest single value that will be treated as a code. Defaults to 12.
	MaxCodeLength int

	// RepeatHeadroom multiplies the observed maximum repeat count. Defaults to 2.
	RepeatHeadroom int

	// Source names where the profile came from, for the DerivedFrom field.
	Source string
}

PromoteOptions tunes what becomes an expectation.

type Result

type Result struct {
	// Messages is how many were examined.
	Messages int `json:"messages"`

	// Judged is false when there were too few messages to conclude anything. Distinguished from "no
	// violations", because they mean opposite things and look identical in a summary.
	Judged bool `json:"judged"`

	// Note explains a result that cannot be trusted.
	Note string `json:"note,omitempty"`

	// Violations are ordered worst first, by how far from the threshold they fell.
	Violations []Violation `json:"violations"`

	// Checked is how many expectations were evaluated, so a passing result says what it proved rather than
	// only that nothing was wrong.
	Checked int `json:"checked"`

	// Unjudged names expectations that could not be evaluated at all, each with the reason.
	//
	// Separate from Violations because they are not findings about the feed, they are findings about the
	// contract - and separate from Checked because counting them there is the lie this exists to correct. An
	// expectation nothing could be judged against was previously counted as checked and produced no violation,
	// so it read as a passing check and had never run once.
	Unjudged []string `json:"unjudged,omitempty"`
}

Result is the outcome of checking a contract.

func Check

func Check(c *Contract, p *profile.Report) *Result

Check evaluates a contract against a profile.

Against a profile rather than against messages, deliberately. The profile already counts fill rates, cardinality and code sets over a corpus, so checking against it reuses that work, means a contract and a profile can never disagree about what was observed, and keeps this package away from message bytes entirely - which is also why it cannot leak patient data.

func (*Result) Holds

func (r *Result) Holds() bool

Holds reports whether the contract held.

A result that could not be judged does not hold, because saying it does would turn "we have no evidence" into "everything is fine" - which is the failure mode of every monitoring system that reports green when its input has stopped.

func (*Result) Summary

func (r *Result) Summary() string

Summary is the one line to log or show.

type Rule

type Rule string

Rule is what is expected of a path.

const (
	// Populated requires the field to carry a value in at least MinRate of messages.
	//
	// The commonest expectation by a wide margin: nearly every real incident is a field that used to be
	// there and stopped.
	Populated Rule = "populated"

	// Absent requires the field to be empty or missing in at least MinRate of messages.
	//
	// Worth having because a field that starts arriving is as much of a change as one that stops. A sender
	// that begins populating PID-19 with a national identifier has changed what the site is holding, and
	// somebody should know.
	Absent Rule = "absent"

	// OneOf requires the value, when present, to be in Values.
	//
	// The value set is where a strict downstream mapping breaks: a new sex code, a new patient class, a new
	// order status. Those reject at the far end and the rejection rarely says which value caused it.
	OneOf Rule = "one of"

	// SegmentPresent requires the segment to appear in at least MinRate of messages.
	SegmentPresent Rule = "segment present"

	// MaxRepeats requires the field or segment to repeat no more than Limit times.
	//
	// A repetition count that grows is the failure that overflows a fixed-width downstream table, and it
	// happens without any field changing shape.
	MaxRepeats Rule = "at most"
)

type Violation

type Violation struct {
	Expectation Expectation `json:"expectation"`

	// Actual is the observed rate, or the observed repeat count for MaxRepeats.
	Actual float64 `json:"actual"`

	// Messages is how many messages the observation rests on. Reported because a rate from 120 messages and
	// the same rate from 120,000 justify very different responses.
	Messages int `json:"messages"`

	// Unexpected lists values seen that the contract does not permit, for OneOf. Codes rather than free text,
	// so this does not become a way to read patient data - a rule with a permitted value list is by
	// definition about a code set.
	Unexpected []string `json:"unexpected,omitempty"`

	// Says is the sentence to show somebody. Built here rather than at the point of display so every caller -
	// the CLI, the API, an alert webhook - says the same thing.
	Says string `json:"says"`
}

Violation is one expectation that did not hold.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL