Skip to content
🤖 AI-Optimized Docs: Plain text context available at /llms-full.txt
GitHub

Ports, Adapters, and the Conformance Suite

Ports, adapters, and the conformance suite

Section titled “Ports, adapters, and the conformance suite”

eval-quality performs no effects of its own. A port is an interface the package declares for an effect it will not perform; an adapter is your implementation of one. eval-quality/adapters ships three reference adapters, and eval-quality/conformance ships the suite that decides whether an implementation conforms.

Two ports are wired to stages today. runPreflight reaches a live environment through EnvironmentProbePort. runScore resolves a private artifact reference through CorpusPort, and awaits it only when a --private-manifest entry or a private-storage isolation-manifest reference needs its bytes digested and checked; the CLI’s score builds that port from --corpus-root with the shipped local corpus adapter. The other two are declared, published, and unused by the package itself: the CLI reads and writes artifacts with node:fs/promises directly. They are here for a caller building its own pipeline on the library.

PortWhat it doesWired today
EnvironmentProbePortProbes a live environmentYes. runPreflight awaits it
ClockPortReads the current timeNo
CorpusPortResolves an opaque private reference to bytesYes. runScore awaits it when a private reference needs its digest checked
FileSystemPortReads and writes filesNo. The CLI uses node:fs/promises

Each port’s type comes from eval-quality/conformance:

import type { CorpusPort, EnvironmentProbePort } from 'eval-quality/conformance'
import {
createLocalCorpusAdapter,
createNodeFileSystemAdapter,
createSystemClockAdapter,
} from 'eval-quality/adapters'

Each is a factory taking the mechanism it wraps, so a test can substitute one without a filesystem or a clock. The architecture calls them conveniences, and the package works without them. There is no reference EnvironmentProbePort, because probing a live environment is the part only the adopter can write.

The suite checks behavior the type checker cannot: whether the implementation returns a typed fault where the boundary demands one, and whether it hangs where it should time out.

It does not take your port. It takes a PortSubject, which is a small harness around your port that the suite drives. You supply a name, one sample request, and a build function the suite calls once per scenario. ScenarioKind is the four situations it needs your port to be in: resolves, fails, in-band-error, and hangs. Putting the port into each of them is your job, because only you know how to make your mechanism fail.

import {
runCorpusPortConformance,
formatConformanceReport,
type PortSubject,
type ScenarioKind,
} from 'eval-quality/conformance'
const subject: PortSubject<{ privateRef: string }> = {
name: 'my-corpus-adapter',
sampleRequest: { privateRef: 'ref-1' },
build: async (scenario: ScenarioKind) => {
let calls = 0
return {
port: async (request, signal) => {
calls += 1
return myMechanism(request, signal, scenario)
},
underlyingCalls: () => calls,
}
},
}
const report = await runCorpusPortConformance(subject)
console.log(formatConformanceReport(report))
if (!report.passed) process.exitCode = 1

build returns a fresh instance every time. underlyingCalls counts calls into the underlying mechanism; counting the port call itself makes the retry assertion pass for everything.

There is one runner per port: runClockPortConformance, runCorpusPortConformance, runFileSystemPortConformance, and runEnvironmentProbePortConformance.

A report carries subject (the name you gave), port, one outcome per assertion, and a passed field over all of them. Every outcome has an id of the form <method>/<assertion>, so a failure names the method and the property it broke.

CONFORMANCE_OUTCOME_COUNTS publishes the count per port, and the suite asserts its own totals against it:

PortOutcomes
corpus6
clock6
file-system12
environment-probe19

Six assertions per port method: one per scenario above, plus two over the fault’s type and its artifact path. The environment probe carries thirteen more, from the default-deny target policy that keeps a probe from reaching anything the caller did not authorize.

A count that does not match means the suite did not finish, which is itself a failure. Read the outcome list.

In this repository:

Terminal window
npm run test:conformance

That runs every published runner against the three shipped adapters and against an in-repository probe subject, which exists so the environment-probe runner has something to exercise without a live system.