The README's install line failed: the package was never published, and nothing told npm where the @extant2000 scope lives. A tag like v1.2.3 now publishes to this project's registry with CI_JOB_TOKEN, and the README sets the scope's registry before installing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.5 KiB
ContractCheck
Catches interface drift between two systems: a response whose shape changed, a requested field that never came back, and endpoints with no contract check. It is used in Hitt Hosting products.
What it does and why
A response can be perfectly valid while the field you asked for is
missing. Stripe removed invoice.payment_intent in its 2025-03-31 (Basil)
API version, and Stripe ignores an unknown expand path instead of rejecting
it. A stale expand keeps succeeding and returns a well-formed invoice without
the field. There is no error and no warning; the client secret is absent and
the payment form cannot mount, while typecheck, lint and the test suite all
pass. A schema validator cannot catch this, because the invoice is valid. The
only way to detect it is to check that what you requested actually came back.
ContractCheck has three parts.
assertShape()validates a payload against any schema with a zod-stylesafeParseand throws aContractDriftErrornaming the context and the offending paths. It fails loudly because the bug it targets is an endpoint dropping a field the UI needs: nothing errors, the page renders, and the number is just wrong. A 500 naming the path beats a plausible screen. The error is a plainError, not tied to a web framework, so you map it to your own transport.expectFields()andassertFields()check that each requested dot path is present in the response. An explicitnullcounts as present:nullmeans the provider answered "no value", whileundefinedmeans it never acknowledged the request. Awhynote on each expectation is quoted in the failure, so it says what breaks and not only what is missing.coverage()measures how much of an API surface is contract-checked, against a floor you can raise over time so coverage cannot slip back. It lists the uncovered endpoints, because a bare percentage tells you there is a gap, not where it is.
The Parser type accepts Zod 4 schemas, whose issue paths may include
symbols, and symbol path segments are rendered without crashing.
Reports
fieldReport() and formatFields() make each requested path its own
finding, so "the response is a valid object" and "the field you asked for is
absent" never collapse into one verdict. If no fields were asserted, the
report is not assessed.
coverageReport() and formatCoverage() report an empty surface as not
assessed, not 0%. "0% covered" for a surface with no endpoints describes a
failure that did not happen. Uncovered endpoints are listed one by one as not
assessed. Reports use the @extant2000/evidence-record format.
Usage
import { assertShape, expectFields, assertFields, formatFields, coverage, formatCoverage } from '@extant2000/contract-check'
import { z } from 'zod' // or any parser with a zod-style safeParse
// 1. Response shape
const payload: unknown = { id: 'm1', name: 'Survey' } // e.g. a parsed response body
const Mission = z.object({ id: z.string(), name: z.string() })
const mission = assertShape(Mission, payload, 'GET /missions/:id')
// 2. Requested fields actually returned
const subscription = { id: 'sub_1', latest_invoice: { id: 'in_1' } } // response from the provider
const expectations = [
{ path: 'latest_invoice.payment_intent', why: 'the client secret is needed to mount the payment form' },
'id',
]
const result = expectFields(subscription, expectations)
console.log(formatFields(result, { context: 'stripe.subscriptions.create', expectations }))
assertFields(subscription, expectations, 'stripe.subscriptions.create') // or throw
// 3. Coverage against a floor
const c = coverage(['GET /a', 'GET /b', 'POST /c', 'GET /d'], ['GET /a', 'GET /b', 'POST /c'], 80)
console.log(formatCoverage(c, 80))
For a subscription whose invoice lacks payment_intent, the field report
fails at critical severity on latest_invoice.payment_intent, quotes the
why note, and passes id. The coverage example prints:
✗ HIGH 3 of 4 endpoints contract-checked (75%) [NOT MET]
Below the ratchet floor of 80%.
evidence: Contract coverage = 75% (4 samples)
· MEDIUM GET /d has no contract check [NOT ASSESSED]
ContractCheck does not depend on zod. Any object with a compatible
safeParse works.
Install
The package is published to the GitLab package registry, so point the
@extant2000 scope there first:
npm config set @extant2000:registry https://gitlab.com/api/v4/packages/npm/
npm install @extant2000/contract-check
License
MIT. See LICENSE.