7. The operation
interface Operation { kind: 'httpOperation'; id: string; name: Name; // 'assignTask' surface: SurfaceId; op?: 'list' | 'get' | 'create' | 'update' | 'delete' | 'batch'; // the ops shorthand it came from resource?: { type: string; idParam?: string; idProperty?: string }; // the collection or single it belongs to: '$collection' value, member id parameter, item id property features: Feature[]; // must-understand features this operation uses (ADR 0005) method: 'get' | 'post' | 'put' | 'patch' | 'delete'; path: string; // '/v1/channels/{channelId}/tasks/{taskId}/assign' safe: boolean; // a GET, or a batch read sent as POST params: { path: Param[]; query: Param[]; header: Param[] }; // declared and derived, each with origin or derivedFrom unknownQuery: { policy: 'refuse' } | { policy: 'ignore'; reason: Reason; origin: Origin }; // undeclared query keys access: Access; callerKinds: ('anonymous' | 'identified')[]; principalRequires: { field: string; origin: Origin }[]; scopes: { scope: ScopeId; source: ValueSource | { resolver: true }; origin: Origin }[]; // resolution order requires: Requirement[]; // pack-derived (pack, then entry order), then ancestors', then the route's own (ADR 0002 section 8) bindings: { from: ValueSource; to: ValueSource; origin: Origin }[]; input?: { media: string; // 'application/json', 'application/x-www-form-urlencoded', 'multipart/form-data' schema: TypeUse; bodyLimitBytes: number; rawBody: boolean; fields?: { name: string; wire: QueryWire | { kind: 'file' } | { kind: 'json' } }[]; // form and multipart: per top-level field proofs?: { name: string; schema: TypeUse; wire?: QueryWire | { kind: 'json' }; derivedFrom }[]; // body rule proofs (section 13); wire: form and multipart only origin }; responses: Response[]; // success and derived non-error responses (304), sorted by status errors: { code: ErrorCode; example?: unknown; derivedFrom?: DerivedFrom; origin?: Origin; // origin: the node that attached it (A35) roles?: { role: ErrorRole; params?: Template }[]; // irRevision 4: the roles this code answers here, sorted by role retryHeader?: { name: string; param: string; unit: 's' | 'ms'; derivedFrom } }[]; // irRevision 4: a bucket rule's 429 fieldRights: FieldRight[]; // empty on a relay route: the public operation projects (ADR 0002 section 7) idempotency?: { header: string; replayHeader: string; lifetimeMs: number; required: boolean; enforce: 'boundary' | 'none'; derivedFrom } | { exempt: Reason; origin }; concurrency?: { version: ValueSource; field: string; derivedFrom }; // per operation: a DELETE reads a body source from the query owner?: { kind: 'caller'; origin }; cache?: Cache; paging?: Paging; filteredBy?: RightId; degradable?: { paths: string[]; header?: string; origin }; // header: irRevision 4 budgetMs?: number; deadlineHeader?: string; deprecated?: { since: string; sunset: string; successor?: OperationRef; successorRel?: string; origin }; // since, successorRel: irRevision 4 relays?: OperationRef; // a relay route: its shape fields carry derivedFrom 'relay.route' upstream: ServiceId[]; // owner first maturity: { level: Maturity; source: 'api' | 'upstream' | 'stated'; reason?: Reason; origin }; internal: boolean; csrf?: { header: string; derivedFrom } | { exempt: Reason; origin }; format: FormatId; tags: string[]; examples: Record<string, { status?: number; request?: true; error?: ErrorCode; value: unknown }>; // error: value holds the code's params docs; extensions; origin;}
type Access = | { kind: 'identity'; identity: IdentityId; schemes: SchemeId[]; origin } | { kind: 'optional'; identity: IdentityId; schemes: SchemeId[]; reason: Reason; onRefused: 'anonymous' | 'reject'; origin } // a refused credential: anonymous, or 401 | { kind: 'public'; reason: Reason; origin };
type Requirement = | { kind: 'right'; right: RightId; params?: unknown; subject?: { source: ValueSource; defaulted: boolean }; entry?: string; origin?: Origin; derivedFrom?: DerivedFrom } // entry: the 'requirements' entry it was written through | { kind: 'rule'; rule: RuleId; params?: unknown; bucket?: BucketId; entry?: string; origin?: Origin; derivedFrom?: DerivedFrom };
interface Response { status: number; media?: string; // absent for an empty body (204, 304, a redirect); a download's media list is in its file type body?: TypeUse; // the unwrapped payload, or a file for a download shape: 'item' | 'table' | 'list' | 'page' | 'empty' | 'binary' | 'redirect'; // the format's shape; 'binary' and 'redirect' bypass the format headers: { name: string; type: TypeUse; required: boolean; semantic?: ResponseHeaderSemantic; origin?: Origin; derivedFrom?: DerivedFrom }[]; docs?: Docs; // the docs written on the response (irRevision 3) origin?: Origin; derivedFrom?: DerivedFrom;}
interface FieldRight { // FR-23: materialised so targets never walk schemas schema: SchemaId; pointer: string; // '/effort', '/items/*/effort' right: RightId; params?: unknown; subject?: { source: ValueSource | { holderField: string } }; // derived from the right's declaration; absent for a right without subject always?: 'absent'; // irRevision 4: the field is never sent here, no decide call direction: 'input' | 'output'; inputPolicy?: 'reject' | 'strip'; derivedFrom: DerivedFrom;}
interface OperationRef { kind: 'httpOperation'; api: string; id: string }type ValueSource = { in: 'path' | 'query' | 'header' | 'body' | 'principal' | 'scope'; name: string } | { in: 'host' | 'subdomain' | 'address' } | { in: 'address'; prefixBits: number };-
The operation holds everything a target renders: identity and schemes, CSRF, the ordered requirements, scopes, field rights, headers, errors, idempotency, cache, paging. Targets render and never re-derive.
-
requiresis a union tagged bykind, keeping the declared order (FR-5, SCN-009). Each item’sparamsare values already validated against the right’s or rule’s params schema. -
errorslists every code the operation can answer, declared or derived, each code once (SCN-011). The status of each is read from the registry; a target builds its status union fromresponsesand the statuses oferrors. The undeclared-status 500 is listed like any other code. -
Operation references carry the declaration kind (ADR 0014), so hook requests and
relaysstay valid when other kinds exist. -
Query parameters carry their wire form:
{ name, type, required, wire: { kind: 'number' | 'boolean' | 'list' | 'text' | 'object'; style: 'form' | 'deepObject'; explode: boolean; brackets: boolean }, default?, origin }(FR-30). Thatwirevalue is theQueryWirethat form and multipart fields reuse;bracketsmarks a list sent asname[](ADR 0002 section 7), which the document writes as a parameter namedname[]. -
A field right with
always: absent(irRevision 4) is an output right whose field is never sent on this operation: it carries nosubject,inputPolicyorparams, and the target makes no decide call (ADR 0003 section 6). Withoutalways, a field right carriessubjectif and only if its right’s subject kind is notnone(invariantir.field-right.always). -
A sort key’s
paramsandsubject(irRevision 4) sit only with itsright: the right’s params, validated against its params schema, and where its subject is read on the request, the route’s resolved scope or its default subject, never a holder field;subjectis present if and only if the right’s subject kind is notnone(invariantir.sort-key.right). -
degradable.header(irRevision 4) names the header listing the degraded parts: required when the operation’s format has nodegradedmeta member (invariantir.degradable.header); the compiler writes it beside one only whenconventions.degradedHeaderis set, and then the target fills both. Its value is the paths ofdegradable.pathsdegraded in this response, as written, inpathsorder, an RFC 9110 section 5.6.1 list joined by,; the header is left out when no part is degraded. -
A deprecated operation’s 2xx responses each carry one header of semantic
deprecation, one ofsunset, and one ofsuccessorLinkif and only ifdeprecated.successoris set, which comes withsuccessorRel, and no other response carries a header of these semantics: a redirect or a 304 is no success (invariantir.deprecation.headers). A successor in the same api names an operation of the document, and each of its path params is a path param of the deprecated operation with the same type use; a successor in another api is checked at compile time only. The values are computed from the date’s fields, never the machine’s zone:deprecationis@<unix seconds of since at 00:00:00 UTC>(RFC 9745);sunsetis the IMF-fixdate ofsunsetat 00:00:00 GMT (RFC 8594, RFC 9110 section 5.6.7);successorLinkis<target>; rel="<successorRel>"(RFC 8288), the target the successor’s path with each path param the deprecated request’s param of the same name, its decoded value percent-encoded as one path segment, every octet of its UTF-8 bytes outside RFC 3986unreservedencoded (a%too, so an encoded value is encoded again; a.or..value stays as is, and the link resolves on the same origin), never the raw or decoded text, a relative reference underapi.wire.urls: relativeand absolute from the configured origin underabsolute(section 13). -
deprecated.since(irRevision 4) is the day the operation is deprecated;sinceandsunsetare calendar dates, real days of the proleptic Gregorian calendar from 0001 to 9999,sincenot aftersunset(invariantir.deprecation.since). -
An error element’s
roles(irRevision 4) say which roles of ADR 0006 section 6 the code answers on this operation, a role at most once across the operation’s elements (invariantir.operation.role-once);ErrorDecl.defaultonly marks the core default. A binding’sparamsis a compiledTemplate(section 8), present if and only if the code declares params, with noeachinside anothereach’sas(invariantir.error.role-params); a binding without an authored template holds the by-name object template: one member per param of the code that has a datum of the same name for the role, in the params schema’s property order, each the slot of that datum. A target renders it with grammar 13 “Templates”, the slots being the role’s data plusiteminsideeach, except: a slot without a value has none, and a member without a value is omitted whatever the format’sabsent; a text template with a part without a value has none, and a number or boolean part is its JSON text; an array item without a value is dropped;eachover an empty array gives[], over a slot without a value none;mergeof a slot without a value adds no member; an object left with no member has no value, except the top-level params object, kept as{}. The rendered params validate against the code’s params schema, and the format’s$paramsslot holds them; a binding whoseroleis unknown is named and refused where the target acts on it. -
An error element’s
retryHeader(irRevision 4) sits only on a code of status 429 derived fromrule.bucket; itsparamis a required, non-nullableintegermember of the code’s params withminimumat least 0 andmaximumat most 9007199254740991 (invariantir.error.retry-header). The target writes the header on every response of that code on the operation, a handler’s typed error result included, from the rendered params’parammember: undermsits decimal integer, undersceil(ms / 1000)as a decimal integer, never an exponent form.
interface Cache { family: string; directives: { maxAgeMs: number; sharedMaxAgeMs?: number; staleWhileRevalidateMs?: number; staleIfErrorMs?: number; immutable: boolean; noCache: boolean; mustRevalidate: boolean } | null; // null for 'never' (no-store) anonymous: 'public' | 'private'; vary: string[]; etag: boolean; perCallerKind: { caller: 'anonymous' | 'identified'; cacheControl: string }[]; // what OpenAPI lists noStore: boolean; // true when 'never' or a response carries a sensitive value origin; derivedFrom?;}interface Paging { style?: PagingId; // absent for an unpaged list ('paging: none') maxLimit?: number; defaultLimit?: number; overLimit: 'reject' | 'clamp'; token?: 'opaque' | 'instant'; sortKeys: { key: string; right?: RightId; params?: unknown; subject?: FieldRightSubject; // params, subject: irRevision 4 derivedFrom?: DerivedFrom; origin?: Origin }[]; origin;}Cache.noStoreis set on an operation whose responses carry a sensitive value (ADR 0003 section 6, invariantir.cache.sensitive-no-store): a property marked sensitive, or a scalar use marked sensitive (section 5), reached from a body through array items, JSON content, object properties, map keys and values and union variants, or from a response header’s type. Without acache, the compiler writes{ family: never, noStore: true }located at the value, derivationcache.sensitive. A value outside a field of the body (a whole scalar body or its items, a map key, a response header) is no-store underconventions.cache.sensitiveElsewhere: noStore, the default, and a compile error underrefuse(ADR 0002 section 13), so the IR never holds one withoutnoStore. An error code whose params carry a sensitive value follows the same convention throughErrorDecl.noStore(section 4, invariantir.error.sensitive-no-store): a target writesCache-Control: no-storeon every response of that code, whatever the operation’scache.