First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:56:12 -04:00
co-authored by Claude Opus 5.5
commit 7646fe162d
14 changed files with 2253 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
/**
* Detect when two subsystems' interface assumptions drift.
*
* Two design points:
*
* 1. **Framework-agnostic.** A helper that throws a framework-specific HTTP
* error ties itself to that framework. This throws a plain
* `ContractDriftError` the caller maps to its own transport.
* 2. **Request contracts, not just response shapes.** A shape validator
* checks what came back. It cannot catch a provider that SILENTLY IGNORES
* part of the request — see `expectFields` below.
*/
/** Anything with a zod-style `safeParse`. Avoids depending on zod itself. */
export interface Parser<T> {
safeParse: (data: unknown) => { success: true, data: T } | {
success: false
error: {
/**
* Zod 4 widened issue paths to `PropertyKey[]`, which includes symbol.
* Narrowing to string|number here made a Zod 4 schema unassignable to
* this interface — and where typecheck gates a pipeline, that failure
* can block a deploy without anyone reading the log.
*/
issues: Array<{ path: ReadonlyArray<PropertyKey>, message: string }>
}
}
}
export class ContractDriftError extends Error {
readonly context: string
readonly issues: string[]
constructor(context: string, issues: string[]) {
super(`Response shape drift in ${context} — ${issues.slice(0, 3).join('; ')}`)
this.name = 'ContractDriftError'
this.context = context
this.issues = issues
}
}
/**
* Validate a payload against a schema, throwing on drift.
*
* Fails LOUDLY on purpose. The bug class this exists for is an endpoint
* silently dropping a column the UI needs: nothing errors, the page renders,
* and the number is just wrong. A 500 naming the offending path is strictly
* better than a plausible screen.
*/
export function assertShape<T>(schema: Parser<T>, data: unknown, context: string): T {
const result = schema.safeParse(data)
if (!result.success) {
// `.join()` throws on a symbol element, so map through String first —
// a symbol path renders as "Symbol(x)" rather than crashing the handler
// that was trying to report a validation error.
const issues = result.error.issues.map(i => `${i.path.map(String).join('.')}: ${i.message}`)
throw new ContractDriftError(context, issues)
}
return result.data
}
export interface FieldExpectation {
/** Dot path, e.g. `latest_invoice.confirmation_secret`. */
path: string
/** Human note on why this field was requested. */
why?: string
}
export interface FieldCheckResult {
ok: boolean
missing: string[]
present: string[]
}
function readPath(obj: unknown, path: string): unknown {
return path.split('.').reduce<unknown>(
(acc, k) => (acc == null || typeof acc !== 'object' ? undefined : (acc as Record<string, unknown>)[k]),
obj,
)
}
/**
* Assert a response actually contains the fields the request asked for.
*
* THE FAILURE THIS EXISTS FOR: Stripe removed `invoice.payment_intent` in the
* 2025-03-31 (Basil) API version, and Stripe **silently ignores an unknown
* `expand` path** rather than rejecting it. So a stale expand keeps succeeding
* and simply returns an invoice without the field. No error, no warning — the
* client secret is absent and the payment form cannot mount, while typecheck,
* lint and a full test suite all pass.
*
* A response-shape validator cannot catch this: the response is a perfectly
* valid invoice. The only way to detect it is to assert that what you ASKED FOR
* actually came back.
*/
export function expectFields(
data: unknown,
expectations: Array<string | FieldExpectation>,
): FieldCheckResult {
const missing: string[] = []
const present: string[] = []
for (const e of expectations) {
const path = typeof e === 'string' ? e : e.path
if (readPath(data, path) === undefined) missing.push(path)
else present.push(path)
}
return { ok: missing.length === 0, missing, present }
}
/**
* Throwing form of `expectFields`.
*
* Use at an integration boundary where a silently-ignored request parameter
* would otherwise surface as a confusing downstream failure far from its cause.
*/
export function assertFields(
data: unknown,
expectations: Array<string | FieldExpectation>,
context: string,
): void {
const r = expectFields(data, expectations)
if (r.ok) return
const notes = expectations
.filter((e): e is FieldExpectation => typeof e !== 'string' && !!e.why)
.filter(e => r.missing.includes(e.path))
.map(e => `${e.path} (${e.why})`)
throw new ContractDriftError(
context,
notes.length ? notes : r.missing.map(m => `${m}: requested but absent from the response`),
)
}
export interface CoverageReport {
total: number
covered: number
/** 0–100, one decimal. */
pct: number
uncovered: string[]
/** True when below the floor — the caller decides whether that fails a build. */
belowFloor: boolean
}
/**
* Measure how much of a surface is contract-checked, against a ratchet floor.
*
* Raise the floor over time (a ratchet) so coverage cannot silently regress. Reporting `uncovered` rather than just a percentage is deliberate: a
* bare number tells you there is a gap, not where it is.
*/
export function coverage(
all: string[],
checked: Iterable<string>,
floorPct = 0,
): CoverageReport {
const set = new Set(checked)
const uncovered = all.filter(x => !set.has(x))
const covered = all.length - uncovered.length
const pct = all.length === 0 ? 0 : Math.round((covered / all.length) * 1000) / 10
return { total: all.length, covered, pct, uncovered, belowFloor: pct < floorPct }
}
export { fieldReport, coverageReport, formatFields, formatCoverage } from './report.js'
export type { FieldReportOptions } from './report.js'
+115
View File
@@ -0,0 +1,115 @@
import {
type CapabilityReport,
type Finding,
type FormatOptions,
evidence,
formatReport as renderReport,
} from '@extant2000/evidence-record'
import type { CoverageReport, FieldCheckResult, FieldExpectation } from './index.js'
export interface FieldReportOptions {
/** The call being checked, e.g. "stripe.subscriptions.create". */
context?: string
/** The expectations, so a finding can quote why a field was requested. */
expectations?: Array<string | FieldExpectation>
}
const why = (path: string, exps: Array<string | FieldExpectation> = []): string | undefined => {
const e = exps.find(x => typeof x !== 'string' && x.path === path)
return typeof e === 'object' ? e.why : undefined
}
/**
* Report a field-presence check.
*
* The distinction this library exists for survives into the vocabulary: a
* response can be a perfectly valid object (schema PASSES) while the field
* you asked for is absent (assertion FAILS), because the provider silently
* ignored part of the request. Each requested path is its own finding so the
* two cannot be collapsed into one verdict.
*/
export function fieldReport(r: FieldCheckResult, opts: FieldReportOptions = {}): CapabilityReport {
const context = opts.context ?? 'response'
const findings: Finding[] = [
...r.missing.map((p): Finding => ({
id: `missing:${p}`,
summary: `${p} was requested but is absent from the response`,
determination: 'fail',
severity: 'critical',
detail: why(p, opts.expectations)
?? 'The provider may be silently ignoring this request parameter. A schema check cannot catch this: the response is a valid object.',
evidence: [evidence.document(context, p, undefined, 'requested, not returned')],
})),
...r.present.map((p): Finding => ({
id: `present:${p}`,
summary: `${p} present`,
determination: 'pass',
severity: 'info',
evidence: [evidence.document(context, p, undefined, 'requested and returned')],
})),
]
const total = r.missing.length + r.present.length
return {
capability: 'ContractCheck',
scope: `${total} requested field(s) on ${context}`,
examined: total,
findings,
notes: total === 0
? ['No fields were asserted, so nothing was checked. A response that is never inspected cannot be said to conform.']
: [],
}
}
/**
* Report contract coverage against a ratchet floor.
*
* An empty surface is `not-assessed`, not 0%. `coverage([], …)` correctly
* returns zero, but rendering "0% covered" for a surface with no endpoints
* describes a failure that did not happen — the same error as reporting 100%
* uptime from zero samples, inverted.
*/
export function coverageReport(c: CoverageReport, floorPct = 0): CapabilityReport {
if (c.total === 0) {
return {
capability: 'ContractCheck',
scope: 'no surface supplied',
examined: 0,
findings: [],
notes: ['No endpoints were supplied, so coverage could not be computed. This is not 0% coverage — nothing was measured.'],
}
}
return {
capability: 'ContractCheck',
scope: `${c.total} endpoint(s)`,
examined: c.total,
findings: [
{
id: 'coverage',
summary: `${c.covered} of ${c.total} endpoints contract-checked (${c.pct}%)`,
determination: c.belowFloor ? 'fail' : 'pass',
severity: c.belowFloor ? 'high' : 'info',
detail: c.belowFloor ? `Below the ratchet floor of ${floorPct}%.` : undefined,
evidence: [evidence.measurement('Contract coverage', c.pct, c.total, { unit: '%' })],
},
// Named individually rather than left as a percentage: a bare number
// tells you there is a gap, not where it is.
...c.uncovered.map((path): Finding => ({
id: `uncovered:${path}`,
summary: `${path} has no contract check`,
determination: 'not-assessed',
severity: 'medium',
evidence: [],
})),
],
}
}
export function formatFields(r: FieldCheckResult, opts: FieldReportOptions & FormatOptions = {}): string {
return renderReport(fieldReport(r, opts), opts)
}
export function formatCoverage(c: CoverageReport, floorPct = 0, opts: FormatOptions = {}): string {
return renderReport(coverageReport(c, floorPct), opts)
}