@visionary_software/contrax-annotations
v2026.5.10
Published
Contrax SPI vocabulary types — Enforcement, CallSite, Check, Details — for the TypeScript port.
Readme
contrax-annotations — Design by Contract SPI vocabulary (TypeScript line)
The vocabulary layer of the Contrax Design by Contract framework, ported to TypeScript: discriminated-union types (CallSite, Check), the Enforcement strategy, the Details substrate bundle, and the standalone violate / withDetail / formatCallSite / siteFor functions every other contrax-typescript module builds on.
This is the TypeScript line. The Java line lives on java; the Kotlin K2/FIR port on kotlin.
Install
bun add @visionary_software/contrax-annotations
npm i @visionary_software/contrax-annotations
pnpm add @visionary_software/contrax-annotations
yarn add @visionary_software/contrax-annotationsPeer dependency: typescript. Direct dependency: @visionary_software/apt (TS-AST navigation toolkit).
Conceptual model
Design by Contract — formalised by Bertrand Meyer in Object-Oriented Software Construction — divides responsibility between a routine and its caller into three obligations: the caller must satisfy a precondition, the routine must satisfy a postcondition, and every class must maintain its invariants across visible states.
TypeScript has no built-in support. The usual fallback is defensive runtime if-and-throw, scattered across method bodies, indistinguishable from business logic, detecting violations late. Contrax instead lets library authors declare their preconditions and postconditions as JSDoc tags, and a transformer plugin (in the processor module, ported separately) turns violations into build-failing tsc diagnostics at the offending call site or returned expression.
This module is the SPI vocabulary every part of that pipeline shares.
Type catalogue (so far)
| Export | Shape | Role |
|---|---|---|
| CallSite | discriminated union of AtParameter \| InReturn \| WithDetail | Single parameter object handed to an Enforcement. AtParameter for pre-condition seams; InReturn for post-condition seams; WithDetail decorates either with a clarifying message suffix. |
| Check | discriminated union of Pre \| Post | Strategy describing WHAT seam to look at. The dispatch primitive (details.check(check), introduced in a later feature) is the Context that knows HOW to look. |
| Enforcement | (site: CallSite) => void | Compile-time DbC enforcement. Inspects site and calls violate(site) (optionally wrapped via withDetail first) when the contract isn't met. |
| Details | record { executable, sourceFile, program, checker, addDiagnostic } | The substrate bundle a CallSite carries — mirror of Java's record Details(ExecutableElement, CompilationUnitTree, ProcessingEnvironment). |
| formatCallSite(site) | (site) => string | Canonical diagnostic message frame: "@<UserTag> violated at parameter #N ('name' of type T) of Class.method" (AtParameter) / "@<UserTag> violated in return from Class.method" (InReturn) / "<delegate's frame>: <detail>" (WithDetail; stacks). |
| withDetail(site, detail) | (CallSite, string) => WithDetail | Wrap with a clarifying suffix; stacks. |
| violate(site) | (CallSite) => void | Unwraps WithDetail to the leaf for positioning, pushes a ts.Diagnostic (Error severity, fixed CONTRAX_VIOLATION_CODE) on the leaf's expression range with the wrapped site's formatted message. |
| siteFor(check, useSiteTag, details) | (Check, ts.JSDocTag, Details) => CallSite | Build the CallSite flavor matching check. |
| contractTagName(check) | (Check) => "precondition" \| "postcondition" | The JSDoc tag name a Check looks for on candidate declarations. |
| CONTRAX_VIOLATION_CODE | 6_000_001 | Numeric code on every contrax-emitted diagnostic. Sits in the 6_000_000+ range reserved for third-party transformer codes so it cannot collide with TypeScript's own. |
| check(details, strategy) | (Details, Check) => void | The dispatch primitive — mirror of Java's Details.check(Check). Walks getOverrideHierarchy(details.executable), scans tags per candidate (parameter at index for Pre, method tags for Post), finds the contract tag (direct match on the use-site OR indirect via tagsOnRegistrationSite), reads the enforcement name from the contract tag's comment, looks it up in details.enforcements, dispatches enforcement(siteFor(strategy, useSiteTag, details)). First-match-wins; closest-override-wins implicit. |
| Details.enforcements | ReadonlyMap<string, Enforcement> | The Enforcement registry (mirror of Java's static ServiceLoader.load(...).toList()). Keyed by export name (e.g. "IsNotBlank") — the convention contrax-transformer uses when scanning node_modules for packages whose package.json declares a contrax.enforcements field (cross-link: see processor's discovery section). |
Dispatch is silent on missing pieces
check() is permissive on three boundaries that look like they should be loud but aren't:
- No tag at the seam — a parameter with no
@preconditionand no registered custom tag dispatches nothing. There is no contract here to verify. - A tag whose Enforcement isn't in the registry — the use site references
@precondition Foobut noFoowas discovered innode_modules. Build passes silently. Surfacing this as a build failure would couple every consumer's build to the exact set of installed enforcement packages, which is operationally fragile (CI agents that haven't pulled an enforcement plugin would fail builds on otherwise-correct code). - A registered Enforcement that decides the use site doesn't violate — the silent-pass case for built-ins on identifier references / computed values that contrax can't read at compile time.
A separate XMOD-3 meta-diagnostic in processor catches the closely-related "upstream package shipped with removeComments: true" case so it doesn't masquerade as silent dispatch.
Why a separate module?
To keep the contrax SPI clean. contrax-annotations defines the vocabulary types every other contrax module shares, with no transformer / engine / enforcement code. AST navigation belongs in apt; the engine that walks call sites and dispatches details.check(check) belongs in processor; the built-in enforcements belong in enforcements.
TypeScript peculiarities
Every
CallSiteandCheckcarries akinddiscriminant. TypeScript interfaces are erased at runtime — there is noinstanceof AtParameterbecauseAtParameteris not a runtime entity. To distinguish union members at runtime (informatCallSite, insiteFor, in every Enforcement that needs to skip thewithDetailwrapper), an explicit string tag is the idiomatic solution. The compiler then narrows the union insidecase "atParameter":branches automatically. The Java/Kotlin lines lean on sealed-hierarchy pattern matching for the same flow; TS uses string-discriminated unions.Cross-package consumption ships both ESM and CJS. Downstream packages (
contrax-enforcements,contrax-range-check, the transformer's runtime users) import this package as ESM. The transformer itself runs on Node via tspc and discovers Enforcement bundles viarequire(), so every Enforcement-shipping consumer of this package must publish a CJS sibling alongside its ESM. This package's ownpackage.jsonuses the standardexports.import/exports.requireconditions so both consumption paths resolve to the right artifact; downstream packages follow the same pattern. See theprocessorREADME's "TypeScript peculiarities" section for the full discovery walk.
License
GPL-3.0-or-later. See COPYING. Contact Visionary Software Solutions for commercial licensing.
