Declarative Rule Types Reference
Every atomic profile in the validation battery is composed of one or more declarative rule objects. The battery supports twelve typed rule contracts exported from @nhtio/adk/batteries/validation.
export type OrderingRule =
| OrderRule
| RequiredMetadataRule
| AlternationRule
| AdjacencyRule
| PreservationRule
| RoleRemapRule
| StaleContentAdvisoryRule
| IdentifierFormatRule
| NonEmptyTurnRule
| ToolIdentityRule
| SchemaIntegrityRule
| IdentifierUniquenessRuleSeverity Defaults to Advisory
A field severity?: 'blocking' | 'advisory' exists on every rule type (except StaleContentAdvisoryRule, which is inherently advisory). When omitted, severity defaults to 'advisory'.
This default exists because catalog rules were originally derived from vendor documentation, but a live audit measured against native vendor APIs found that 16 of 17 rules blocked turn state the vendor actually accepts (only thought-signature-required was confirmed enforced). Defaulting to advisory ensures the battery reports protocol deviations for observability without rejecting dispatches the model serves. You opt into gating per rule with severity: 'blocking'.
1. OrderRule (type: 'order')
Enforces relative ordering between two primitive categories (before and after).
export interface OrderRule {
type: 'order'
id: string
before: 'message' | 'thought' | 'toolCall'
after: 'message' | 'thought' | 'toolCall'
scope: 'adjacent-same-role-group' | 'entire-turn'
onlyLatestGroup?: boolean
severity?: 'blocking' | 'advisory'
}before/after: Defines the required relative order.scope: When'adjacent-same-role-group', ordering is checked within contiguous role segments. When'entire-turn', ordering spans the full dispatch timeline.onlyLatestGroup: Whentrue, ignores older historical groups and checks only the active turn (e.g. Anthropic's manual thinking rule).severity: Omitted means'advisory'. Opt into'blocking'to reject or repair.- Repair in
mutatemode: Repaired viareorderby adjustingcreatedAton the offending primitive so it sorts immediately before the primitive it must precede.
2. RequiredMetadataRule (type: 'requiredMetadata')
Enforces that a primitive carries specific provider metadata in its payload object.
export interface RequiredMetadataRule {
type: 'requiredMetadata'
id: string
kind: 'message' | 'thought' | 'toolCall'
applyTo: 'first-in-group' | 'every'
requiredPayloadKey: string
severity?: 'blocking' | 'advisory'
gatedByReplayCompatibility?: string[]
fallbackPayloadValue?: unknown
fallbackReplayCompatibility?: string
fallbackRepairAuthorized?: boolean
}applyTo:'first-in-group'targets only the leading primitive in an assistant group;'every'checks all matching primitives.requiredPayloadKey: Dot-path invalue.payload(e.g.'thoughtSignature').severity: Omitted means'advisory'. (Note:thought_signature_requiredsetsseverity: 'blocking'explicitly because it is confirmed enforced by Gemini 3).gatedByReplayCompatibility: Optional wire format tags that restrict when this rule fires.fallbackPayloadValue/fallbackReplayCompatibility: Documented sentinel values used bymutatemode when metadata fallback repair is enabled.fallbackRepairAuthorized: Whentrue, authorizes mutate mode to applyfallbackPayloadValuewithout requiring the globalallowMetadataFallbackRepairswitch. Reserved for vendor-published bypass sentinels (such as Gemini'sskip_thought_signature_validator).- Repair in
mutatemode: Repaired viafill-required-metadatawhen authorized.
3. AlternationRule (type: 'alternation')
Enforces strict role cycling across conversation turns.
export interface AlternationRule {
type: 'alternation'
id: string
roles: ReadonlyArray<'user' | 'assistant'>
mode: 'strict'
maxPerGroup?: number
severity?: 'blocking' | 'advisory'
}roles: Permitted role alternation sequence (normally['user', 'assistant']).mode:'strict'requires every consecutive turn to alternate roles.maxPerGroup: Optional upper bound onToolCallprimitives within a single assistant role group (e.g.1for Llama 3).severity: Omitted means'advisory'. Opt into'blocking'to gate dispatch.- Repair in
mutatemode: Repaired viainsert-alternation-fillerby materializing a temporary syntheticMessagein the opposite role.
4. AdjacencyRule (type: 'adjacency')
Constrains the immediate successor of a primitive. In this ADK, tool execution results are stored directly on ToolCall rather than on separate message payloads, so adjacency rules directly forbid disallowed primitive kinds from appearing immediately after a specified primitive.
export interface AdjacencyRule {
type: 'adjacency'
id: string
first: 'message' | 'thought' | 'toolCall'
disallowBetween: Array<'message' | 'thought' | 'toolCall'>
severity?: 'blocking' | 'advisory'
}first: The primitive kind whose immediate successor is restricted.disallowBetween: Array of primitive kinds that are forbidden from appearing immediately afterfirst.severity: Omitted means'advisory'. Opt into'blocking'to enforce.- Repair in
mutatemode: Repaired viareorder-adjacent. Shifts the disallowed successor'screatedAtso it sorts immediately beforefirst. Every primitive survives without dropping content.
5. PreservationRule (type: 'preservation')
A stateful check that diffs the current dispatch timeline against the previous iteration's snapshot on ctx.stash.
export interface PreservationRule {
type: 'preservation'
id: string
kind: 'message' | 'thought' | 'toolCall'
invariant:
| 'count-non-decreasing'
| 'payload-field-stable'
| 'pruned-after-latest-turn'
payloadField?: string
resetOnModelSwitch?: boolean
severity?: 'blocking' | 'advisory'
}count-non-decreasing: Historical primitive count ofkindmust never decrease.payload-field-stable: Value atpayloadFieldmust remain unchanged across iterations.pruned-after-latest-turn: Pruning is permitted prior to the latest non-tool-call user message; all primitives at or after that boundary must remain present and unchanged.resetOnModelSwitch: Resets the snapshot baseline when the target model changes.severity: Omitted means'advisory'.- Repair in
mutatemode: Never repairable. When history is dropped or altered upstream, the guard refuses to invent lost context. Rejects dispatch if configured asblocking.
6. RoleRemapRule (type: 'roleRemap')
Describes required provider-specific wire role tags for model families with custom role schemas (such as IBM Granite).
export interface RoleRemapRule {
type: 'roleRemap'
id: string
kind: 'message' | 'thought' | 'toolCall'
variant: string
expectedRoleTag: string
severity?: 'blocking' | 'advisory'
}variant: Profile-defined mapping identifier expected on the payload (e.g.'granite-3.x').expectedRoleTag: Dot-path resolved insidevalue.payload(e.g.'roleTag'checkspayload.roleTag). Must not prefix withpayload..severity: Omitted means'advisory'. Becausepayload.roleTagis consumer-supplied, defaulting to advisory prevents rejecting tool calls for callers who do not hand-annotate payloads.
7. StaleContentAdvisoryRule (type: 'staleContentAdvisory')
Non-blocking hygiene recommendation rule that checks for obsolete content without blocking dispatch.
export interface StaleContentAdvisoryRule {
type: 'staleContentAdvisory'
id: string
kind: 'message' | 'thought' | 'toolCall'
scope: 'before-latest-user-turn'
optOutOptionKey: string
}scope: Identifies content predating the latest user turn ('before-latest-user-turn').optOutOptionKey: Identifies the corresponding configuration option (e.g.'preserveThinking') mapping toOrderingGuardOptions.disableAdvisoryRuleIds.- Inherently advisory: Never blocks dispatch, regardless of operating mode.
8. IdentifierFormatRule (type: 'identifierFormat')
Requires a primitive's identifier to satisfy provider length and character set constraints.
export interface IdentifierFormatRule {
type: 'identifierFormat'
id: string
kind: 'message' | 'thought' | 'toolCall'
maxLength?: number
allowedPattern?: string
severity?: 'blocking' | 'advisory'
}kind: Primitive category constrained (typically'toolCall').maxLength: Maximum allowed identifier length. For example, OpenAI Codex returns HTTP 400 for IDs longer than 64 characters.allowedPattern: Regular expression character class permitted (e.g.'[A-Za-z0-9_-]'for AWS Bedrock Converse). Anchored automatically by the evaluator.severity: Omitted means'advisory'. Both Codex and Converse fail on every credential when an ID violates these constraints, so a single bad ID can exhaust an entire provider pool.- Repair in
mutatemode: Not auto-repairable (re-identifying active tool calls could break correlation).
9. NonEmptyTurnRule (type: 'nonEmptyTurn')
Requires a conversation turn to carry content the provider can act upon — prose or an adjacent tool call.
export interface NonEmptyTurnRule {
type: 'nonEmptyTurn'
id: string
role: 'assistant' | 'user'
onlyTerminal?: boolean
severity?: 'blocking' | 'advisory'
}role: Role whose turns are constrained ('assistant'or'user').onlyTerminal: Whentrue, evaluates only the final turn in the timeline. Gemini rejects a request whose terminal turn contains onlythought: truewithfinishReason: MALFORMED_RESPONSE. Whenfalse, evaluates every turn matchingrole(Mistral returns HTTP 400 for any empty assistant turn).severity: Omitted means'advisory'.- Repair in
mutatemode: Not auto-repairable.
10. ToolIdentityRule (type: 'toolIdentity')
Requires every replayed ToolCall to name a tool that the current request actually declares in ctx.tools.
export interface ToolIdentityRule {
type: 'toolIdentity'
id: string
severity?: 'blocking' | 'advisory'
}- Evaluator context: Inspects replayed tool calls against
ctx.tools. If no tool registry is provided, the rule skips silently. - Failure mode caught: Gemini matches
functionResponse.nameagainstfunctionDeclarations. A tool result naming an undeclared tool causes Gemini to return HTTP 200 with an empty candidate (parts: [{text: ''}]) and no error. A gateway forwards this asfinish_reason: "stop"withcontent: null, leaving the caller in a silent loop. severity: Omitted means'advisory'. Highly recommended as'blocking'on Gemini pipelines.- Repair in
mutatemode: Not auto-repairable.
11. SchemaIntegrityRule (type: 'schemaIntegrity')
Requires every tool input schema declared in ctx.tools to be internally satisfiable: any property in required must also exist in properties.
export interface SchemaIntegrityRule {
type: 'schemaIntegrity'
id: string
severity?: 'blocking' | 'advisory'
}- Evaluator context: Evaluated against declared tools in
ctx.tools. Skips silently if no tools are passed. - Failure mode caught: An unsatisfiable tool schema causes Amazon Nova to return HTTP 200 with the required field silently omitted from tool call arguments, producing zero errors at any layer.
severity: Omitted means'advisory'.- Repair in
mutatemode: Not auto-repairable (requires tool definition schema correction).
12. IdentifierUniquenessRule (type: 'identifierUniqueness')
Requires identifiers of a primitive kind to be unique across the dispatch timeline. Unlike IdentifierFormatRule — a per-entry predicate over length and character set — this is a cross-entry set property whose finding names a collision group: three calls sharing one id is one defect, not three pairs. The evaluator scans the whole timeline because the collision is definitionally cross-turn; any role- or group-scoped check would partition the colliding pair apart and see nothing wrong in either half.
export interface IdentifierUniquenessRule {
type: 'identifierUniqueness'
id: string
kind: 'message' | 'thought' | 'toolCall'
renameStrategy?: (previousId: string, memberIndex: number) => string
surface?: 'dispatch' | 'turn' | 'both'
severity?: 'blocking' | 'advisory'
}kind: Primitive whose identifiers must be unique (typically'toolCall').renameStrategy: Produces a replacement identifier whenmutatemode repairs a collision. Defaults to() => uuidv6(). Every member of a collision group shares ONE id — that is what makes it a collision — sopreviousIdalone cannot distinguish them:memberIndex(0-based, ordered by timelineseq) is what a deterministic strategy must vary on. Without it a pure strategy returns the same replacement for every member and the materialiser rejects its own repair. The strategy must be pure and total in BOTH arguments, and may not return an id already held anywhere in the timeline — not merely elsewhere in the group; the materialiser verifies both and fails the repair rather than trusting it. A Responses-facing profile supplies a composite-preserving strategy that rewrites only thecallIdhalf of a`${callId}|${itemId}`id and carries a validfc_…item id through, because a bare-uuid replacement would drop the reasoning-item replay link.surface: Which guard surface the rule applies to. Omitted means the rule applies on both surfaces.'dispatch'keeps the rule inert on the turn middleware (where a blocking collision would abort the turn before the dispatch that could repair it ever ran);'both'enables turn-level detection, where the rule reports and, inmutatemode, repairs the collision; underenforceit reports and aborts — ordinary blocking-rule behaviour under enforce, not special to this rule or surface. Defaults to'both'on every other rule type, so existing rules are unchanged.severity: Omitted means'advisory'. The shippedtool_call_id_uniquenessprofile sets'blocking'deliberately — an advisory finding can never be repaired, and reusing an id corrupts result correlation.- Repair in
mutatemode: Repaired viarenumber-colliding-idson the dispatch surface, when the context exposes the group-replacement capability. The repair renames every member of the group (renaming all but one would leave the survivor holding the colliding id, so the rule would fire again next iteration and never converge) and rewrites the ids in durable storage. Where the capability is absent the repair is not attempted and the finding stays unrepaired — which, for a blocking rule, means the dispatch nacks.
See Also
- Validation Hub — Overview of the ordering guard battery.
- Atomic Behaviors — Catalog of the 21 atomic profiles built from these rule types.
- Operating Modes — How each rule type behaves in
enforcevsmutatemode. - Writing a Profile — Step-by-step guide to writing rules and profiles.