Reference

Guide and API

This page covers installing adorn, the full constraint vocabulary, what each projection emits, the knobs, the CLI and the add-ons. At the end is a searchable index of every runtime export.

Getting started

1 · Install

terminal
npm i adorn

The only runtime dependency is tslib. It ships CommonJS with .d.ts types and supports Node.js 20.19 and later (CI runs 20.19, 22 and 24).

2 · Enable decorators

tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }

reflect-metadata is not required. Without it, the generated docs just lose their Type column. The decorator path needs a metadata-emitting compiler (tsc, ts-node, ts-jest), and esbuild, SWC, Vite and Bun don't emit that metadata. The descriptor-first path (Verify, contract(), toolSchemaFrom) works with no flags at all.

3 · Contract a tool

weather.ts
import {
    Contract, Tool, Describe, NotEmpty, OneOf,
    GreaterThanEqual, LessThanEqual, toolSchemaForMethod,
} from 'adorn';

@Contract()
export class WeatherTool {
    @Tool('Look up a weather forecast.')
    getWeather(
        @Describe('the city to look up') @NotEmpty() location: string,
        @OneOf(['celsius', 'fahrenheit']) unit: string,
        @GreaterThanEqual(1) @LessThanEqual(7) days: number,
    ): string {
        return `${days}-day forecast for ${location} in ${unit}`;
    }
}

// Hand this to any function-calling API as the tool's `parameters`:
export const parameters = toolSchemaForMethod(WeatherTool, 'getWeather');

4 · Route the model's call through it

agent.ts
const tool = new WeatherTool();
const args = JSON.parse(toolCall.function.arguments);

try {
    return tool.getWeather(args.location, args.unit, args.days);
} catch (e) {
    // A contract violation: hand the message back to the model so it can retry.
    return `error: ${(e as Error).message}`;
}
Prefer a value to an exception?

Validate the parsed arguments first with Verify.check(ObjectContracts.Fields({...}), 'args', args), which returns {ok, value} or {ok: false, path, expected, message}. See Working with values.

Parameter constraints

Each one is a decorator for a parameter (@Between(1, 10)) and a descriptor in a namespace (NumberContracts.Between(1, 10)). Every predicate is total: a wrong-type value is a violation, never a TypeError.

DecoratorAcceptsSchema
@GreaterThan(n) / @GreaterThanEqual(n)number > n / ≥ nexclusiveMinimum / minimum
@LessThan(n) / @LessThanEqual(n)number < n / ≤ nexclusiveMaximum / maximum
@Between(a, b)a ≤ number ≤ bminimum + maximum
@Integer() · @MultipleOf(n)whole number · divisible by ntype: integer · multipleOf
@MinLength(n) · @MaxLength(n) · @StringMinLength(n)length boundsminLength · maxLength
@NotEmpty()a non-empty stringminLength: 1
@Matches(pattern, flags?)matches the patternpattern
@Email() · @Url()email · URLpattern · format: uri
@Uuid() · @DateTime() · @Ipv4() · @Format(name)validating formats; date-time is strict RFC 3339format
@OneOf([...])strict === membershipenum
@Const(v)exactly vconst

Stacked decorators conjoin and evaluate top to bottom. @Positive() and friends were removed on purpose, because a name can't carry a boundary. Write @GreaterThan(0).

Composition

nested shapes, as data
const member = ObjectContracts.Fields({
    name: StringContracts.NotEmpty(),
    roles: ArrayContracts.ArrayOf(EnumContracts.OneOf(['admin', 'editor', 'viewer']), { minItems: 1 }),
    note: NullableContracts.Nullable(StringContracts.NotEmpty()),
});

Verify.check(member, 'member', { name: 'Ada', roles: ['admin', 'wizard'], note: null });
// { ok: false, path: ['roles', 1], expected: 'one of "admin", "editor", "viewer"', … }
  • @ArrayOf(item, {minItems?, maxItems?}): every element satisfies item. A failure names the element's index.
  • @Dictionary(value): an open map of string keys to values matching value.
  • @Shape(FieldClass): a closed DTO validated against another class's contracted fields.
  • ObjectContracts.Fields({...}): an optional-aware object literal, with no class needed.
  • @Nullable(item): also accepts null. This is different from @Optional, which means the value may be absent.

Markers

MarkerEffect
@Optional()An absent argument is fine, and the parameter is dropped from the tool schema's required. If any inheritance level requires it, it stays required.
@Default(v)Publishes {default: v} into the schema and implies @Optional. It does not inject the value; the host applies it.
@Describe('…')The property's description, which the model reads.
@Tool('…')The tool's top-level description.

Pre- and postconditions

bank.ts
@Contract()
class Bank {
    @Requires((amount: number, balance: number) => amount <= balance, 'cannot overdraw')
    @Returns(NumberContracts.GreaterThanEqual(0))
    @Ensures((result: unknown, amount: number, balance: number) =>
        (result as number) === balance - amount, 'balance drops by exactly the amount')
    withdraw(@GreaterThanEqual(1) amount: number, @GreaterThanEqual(0) balance: number): number {
        return balance - amount;
    }
}

new Bank().withdraw(30, 100);   // 70
new Bank().withdraw(150, 100);  // throws @Requires (caller overdrew)
new Bank().withdraw(0, 100);    // throws @GreaterThanEqual(1) on `amount`
DecoratorWhenBlamesProjects
@EnabledWhen((self, ...args) => bool)before, reads statecallerno
@Requires((...args) => bool)beforecallerno
@Returns(descriptor)after, on the settled valuecalleeyes
@Ensures((result, ...args) => bool)after, on the settled valuecalleeno

All four stack on one signature. A guard that returns false means "not enabled"; a guard that throws is treated as a defect in the guard itself and propagates unchanged.

Field contracts

account.ts
@Contract()
class Account {
    @Enforce.greaterThanEqual(0) balance = 0;   // validated on every assignment
    @Enforce.notEmpty() owner = 'unknown';
}

Enforce has a method for every constraint (greaterThan, between, oneOf, uuid, arrayOf, shape, nullable and so on). CreateFieldDecorator(descriptor) wraps any descriptor, including your own. Field contracts are inherited: a subclass can strengthen them but never weaken them.

Working with values

Use these when there's no signature to decorate. They take the same descriptors and run the same oracles, as functions.

FunctionUse it for
Verify.check(d, name, value)Untrusted input as a value, such as an HTTP 400. Narrows unknown and reports path and expected.
Verify.that(d, name, value)The same check, but it throws, returning the narrowed value on success.
toolSchemaFrom({name, description, params}, opts)Tool schemas whose permitted values are known only at runtime.
contract(spec, fn)Enforcing params, requires, ensures and returns on a plain function, sync or async.
violationFromCheck(result)Emitting a functional refusal on the observation stream.
defineConstraint({...})Your own first-class constraint, which projects like a built-in.
per-session tool
const params = {
    slotId: EnumContracts.OneOf(availableSlots),  // known only now
    partySize: NumberContracts.Between(1, 8),
};
const schema = toolSchemaFrom({ name: 'book', params }, { providerStrict: true });
const reply = Verify.check(ObjectContracts.Fields(params), 'args', modelArgs);

To try this live, use the playground.

Projections

ProjectionOne classWhole registry
LLM tool schema (prescriptive)toolSchemaForMethod(Cls, 'm', {providerStrict?})n/a
JSON Schema 2020-12 (descriptive)schemaForClass(Cls)generateSchema()
Markdown docsrenderMarkdown([documentClass(Cls)])generateMarkdown()
Privacy reviewserializeTelemetry(telemetryForClass(Cls))generateTelemetryReport()

The model types (ClassDoc, MethodDoc, ToolSchema, TelemetryReport and so on) are exported so you can type variables that hold them.

Configuration

Set these once at startup. They're process-level, not per-request.

CallEffect
setViolationPolicy('throw' | 'warn' | handler)'warn' proceeds into the method. Use it to stage a new contract over live traffic.
setEnforcementMode('development' | 'production' | 'auto')Production strips assert-trust contracts. guard contracts stay on.
setDefaultTrust('guard' | 'assert')The default for contracts that don't declare one.
disableGuardEnforcement({confirm, reason})The only way to strip guard contracts.
setValueDisclosure('none' | 'all')Shows raw values in adorn's own output, for local debugging only.
setContractDiagnosticsEnabled(bool)Dev-mode warnings, such as constraints declared without @Contract().

Privacy

Redaction is on by default and needs no annotation. @Sensitive() on a field, parameter or class vetoes disclosure even under setValueDisclosure('all'). @Telemetry.value(), .redact(strategy) and .result() opt values into the observation path.

A permanent gap

transfer(ssn: string) has to be marked on the parameter itself. JavaScript's value semantics break the link back to a @Sensitive field once the primitive is passed.

CLI

terminal
adorn docs   --out CONTRACTS.md src/tools/*.ts   # Markdown documentation
adorn schema --out schema.json  src/tools/*.ts   # JSON Schema (draft 2020-12)
adorn check  tsconfig.json                       # @Sensitive values reaching an adorn emitter

docs and schema load, then project: they require each entry so its decorators run, which means importing executes the module, side effects included. .ts entries need ts-node and typescript in your project. check is static: it never runs your code, and it exits non-zero if it finds a leak.

Add-ons

Both ship inside the package as subpath exports. Importing the root never loads them, and each one needs its peer dependency only when you use it.

adorn/test

Needs fast-check. fuzzMethod, fuzzClass and fuzzFunction (each with an async variant) test one unit against its own contract. fuzzSequence tests call sequences on stateful objects.

npm i -D fast-check

adorn/otel

Needs @opentelemetry/api. createOtelSink() turns the observation stream into spans, a duration histogram and value-free violation events.

setObservationSink(createOtelSink());

Gotchas

  • @Contract() is required. Without it, constraints are neither enforced nor projected. adorn warns in development.
  • Decorator order with @Validate(). Method decorators it should see must sit below it. Class-level @Contract() doesn't care about order.
  • TypeScript defaults are honored. f(@GreaterThanEqual(1000) t = 30_000) called as f() uses the default. @Default exists to publish a default to the schema.
  • A TypeScript default and @Default can disagree. @Default(10) x = 20 publishes 10 and runs with 20. adorn doesn't reconcile the descriptor with TypeScript.
  • No constructor-parameter decorators yet. Use a @Validate()-decorated static of(...) factory with a private constructor.
  • static #private blocks class decorators (TS18036). Move it to module scope.
  • @NotEmpty() accepts whitespace. Pair it with @Matches(/\S/) when blank input should fail.
  • A wrapped method reports length === 0. Its .name is correct, and contract() preserves length.

API index

Every runtime export of adorn, adorn/test and adorn/otel. The site's build check fails if this list drifts from the library. Press / to search.