---
url: 'https://adk.nht.io/batteries/validation/writing-a-profile.md'
description: >-
  How to add new atomic ordering behaviors, register model-family recipes,
  compose rules, and implement extensibility escape hatches in the validation
  battery.
---

# Writing an Ordering Profile & Family Recipe

## LLM summary — Writing an Ordering Profile

* Ordering profiles in `@nhtio/adk/batteries/validation` are **pure declarative data**, never procedural loops or imperative sorting logic.
* Two primary contributor workflows:
  1. **Adding a family recipe** (the 95% case): compose existing atomic behavior tokens into `FAMILY_RECIPES` in `src/batteries/validation/profiles/families.ts`.
  2. **Adding an atomic behavior** (the 5% case): create a new file in `src/batteries/validation/profiles/<behavior_name>.ts` exporting an `OrderingProfile` composed of one or more of the seven declarative `OrderingRule` variants. Register in `profiles/index.ts`.
* The seven typed rule contracts: `OrderRule`, `RequiredMetadataRule`, `AlternationRule`, `AdjacencyRule` (uses `first` and `disallowBetween`), `PreservationRule`, `RoleRemapRule`, `StaleContentAdvisoryRule`.
* Parameterized behaviors use `behavior:argument` tokens in `FAMILY_RECIPES` (e.g. `payload_field_preservation:signature`, `full_history_preservation:thought`).
* **Escape hatches:** Pass ad-hoc `OrderingProfile` objects directly into `profiles: [...]` without waiting for upstream registry releases.

In the `@nhtio/adk` validation battery, **an ordering profile is pure declarative data**.

You do not write procedural loops over message arrays or imperative timestamp comparisons. You declare what invariant must hold over the conversation timeline using the battery's declarative rule types, and the shared evaluation engine and middleware handle validation, snapshotting, repair, and error reporting.

When adding support for a new model or model generation, determine which tier of work is required:

* **Tier 1: Composing a Family Recipe** (the common case) — Add an entry to `families.ts` composing existing atomic behaviors.
* **Tier 2: Adding an Atomic Behavior Profile** — Create a new `.ts` file in `profiles/` when a model has an ordering constraint that maps onto one of the seven rule types but does not yet exist in the catalog.
* **Tier 3: Extensibility Escape Hatches** — Pass ad-hoc profiles in consumer code or extend the rule type union.

***

## 1. Adding a Family Recipe (The Common Case)

Before authoring a new file, check `src/batteries/validation/profiles/families.ts`.

Model family recipes represent real-world model deployments (e.g. `anthropic-manual-thinking`, `gemini-3`, `qwen-3`). A family recipe names the combination of atomic behaviors that the target model family requires.

### The Recipe Syntax

In `src/batteries/validation/profiles/families.ts`, add an entry to `FAMILY_RECIPES`:

```ts
export const FAMILY_RECIPES: Record<string, readonly string[]> = {
  // ... existing recipes
  'my-new-model-family': [
    'strict_alternation',
    'openai_shape_baseline',
    'full_history_preservation:thought',
  ],
}
```

### Parameterized Behaviors

Two atomic behavior profiles in the battery are factories that accept parameters via a colon (`:`) separator in `FAMILY_RECIPES`:

1. **`full_history_preservation:<kind>`**
   * Parameter: `OrderingPrimitiveKind` — `'message' | 'thought' | 'toolCall'`.
   * Example: `'full_history_preservation:thought'` enforces that historical thoughts are never dropped.
2. **`payload_field_preservation:<field>`**
   * Parameter: dot-path into primitive payload (e.g. `signature`, `clear_thinking`, `encrypted_content`).
   * Example: `'payload_field_preservation:signature'` validates that `Thought.payload.signature` remains unchanged across iterations.

Resolution of these tokens is handled automatically when string profile names are passed into the guard options.

### Recipe Authoring Checklist

1. **Naming:** Use lowercase kebab-case (`<family>-<generation-or-mode>`, e.g. `deepseek-v4`, `granite-4-x`).
2. **Cite Vendor Sources:** Add a JSDoc comment above the entry citing the vendor documentation and the date checked.
3. **Handle Documentation Gaps Honestly:** If you are authoring a recipe based on an educated baseline guess rather than a verified spec (as with `bytedance-seed` or `muse-spark`), add an explicit `/** UNCONFIRMED baseline guess ... */` comment. Never dress up an unverified assumption as certainty — a downstream consumer trusting an unverified recipe in `enforce` mode in production will discover your guess the hard way.

***

## 2. Adding an Atomic Behavior Profile

When a vendor introduces an ordering constraint whose semantics are not yet represented in `src/batteries/validation/profiles/`, author a new atomic profile.

An atomic behavior file lives in `src/batteries/validation/profiles/<behavior_name>.ts` and exports an `OrderingProfile` object.

### The Seven Declarative Rule Types

Your profile's `rules` array contains one or more of the seven discriminated union variants defined in `src/batteries/validation/types.ts`:

#### 1. `OrderRule` (`type: 'order'`)

Enforces relative order between two primitive kinds (`'message' | 'thought' | 'toolCall'`).

```ts
import type { OrderingProfile } from '../types'

export const thinkingBeforeToolUse: OrderingProfile = {
  name: 'thinking-before-tool-use',
  description:
    'Within the latest assistant turn, thought must precede toolCall.',
  rules: [
    {
      type: 'order',
      id: 'thinking-before-tool-use',
      before: 'thought',
      after: 'toolCall',
      scope: 'adjacent-same-role-group', // or 'entire-turn'
      onlyLatestGroup: true, // evaluate only the active turn
    },
  ],
}
```

#### 2. `RequiredMetadataRule` (`type: 'requiredMetadata'`)

Enforces that a primitive carries specific provider metadata in its `payload` object.

```ts
export const thoughtSignatureRequired: OrderingProfile = {
  name: 'thought-signature-required',
  description:
    'The first ToolCall must carry a thoughtSignature in its payload.',
  rules: [
    {
      type: 'requiredMetadata',
      id: 'thought-signature-required',
      kind: 'toolCall',
      applyTo: 'first-in-group', // or 'every'
      requiredPayloadKey: 'thoughtSignature',
      severity: 'blocking', // or 'advisory'
      fallbackPayloadValue: 'skip_thought_signature_validator',
      fallbackReplayCompatibility: 'gemini-thought-signature-sentinel-v1',
    },
  ],
}
```

#### 3. `AlternationRule` (`type: 'alternation'`)

Enforces strict turn-to-turn role alternation and optional tool call cardinality bounds.

```ts
export const singleToolCallPerTurn: OrderingProfile = {
  name: 'single-tool-call-per-turn',
  description:
    'Strict alternation with at most one ToolCall per assistant group.',
  rules: [
    {
      type: 'alternation',
      id: 'single-tool-call-per-turn',
      roles: ['user', 'assistant'],
      mode: 'strict',
      maxPerGroup: 1, // Caps ToolCalls per turn (e.g. Llama 3)
    },
  ],
}
```

#### 4. `AdjacencyRule` (`type: 'adjacency'`)

Enforces that disallowed primitive kinds may not immediately follow a specified primitive. In this ADK, tool execution results live directly on `ToolCall`, so adjacency directly constrains immediate successors without requiring field correlation.

```ts
export const openaiShapeBaseline: OrderingProfile = {
  name: 'openai-shape-baseline',
  description:
    'A Message may not immediately follow a ToolCall; tool results live on ToolCall itself.',
  rules: [
    {
      type: 'adjacency',
      id: 'message-not-immediately-after-tool-call',
      first: 'toolCall',
      disallowBetween: ['message'],
    },
  ],
}
```

#### 5. `PreservationRule` (`type: 'preservation'`)

A stateful check that diffs the current dispatch against the previous iteration's snapshot on `ctx.stash`.

```ts
export const reasoningPrunedAfterLatestTurn: OrderingProfile = {
  name: 'reasoning-pruned-after-latest-turn',
  description:
    'Reasoning before latest user turn may drop; recent reasoning must remain unchanged.',
  rules: [
    {
      type: 'preservation',
      id: 'reasoning-pruned-after-latest-turn',
      kind: 'thought',
      // Invariants: 'count-non-decreasing' | 'payload-field-stable' | 'pruned-after-latest-turn'
      invariant: 'pruned-after-latest-turn',
      resetOnModelSwitch: false,
    },
  ],
}
```

#### 6. `RoleRemapRule` (`type: 'roleRemap'`)

Validates custom wire role mappings (e.g. IBM Granite split tool roles).

```ts
export const roleRemapSplitToolRoles: OrderingProfile = {
  name: 'role-remap-split-tool-roles',
  description: 'IBM Granite 3.x wire role mapping validator.',
  rules: [
    {
      type: 'roleRemap',
      id: 'granite-3-x-split-tool-roles',
      kind: 'toolCall',
      variant: 'granite-3.x',
      expectedRoleTag: 'payload.roleTag',
    },
  ],
}
```

#### 7. `StaleContentAdvisoryRule` (`type: 'staleContentAdvisory'`)

Non-blocking hygiene recommendation rule identifying stale content older than the latest user turn.

```ts
export const staleThinkingAdvisory: OrderingProfile = {
  name: 'stale-thinking-advisory',
  description:
    'Gemma 4 hygiene rule advising against resending stale thinking.',
  rules: [
    {
      type: 'staleContentAdvisory',
      id: 'stale-thinking-gemma4',
      kind: 'thought',
      scope: 'before-latest-user-turn',
      optOutOptionKey: 'preserveThinking',
    },
  ],
}
```

***

## 3. Extensibility Escape Hatches

`OrderingProfile` objects are plain JavaScript objects. You do not need to wait for upstream library releases, fork the package, or file an issue to enforce custom constraints or support a newly released model.

### Passing Ad-Hoc Profiles Directly

Consumers can pass ad-hoc profile objects directly into `profiles: [...]`:

```ts
import { orderingGuardDispatchMiddleware } from '@nhtio/adk/batteries/validation'
import type { OrderingProfile } from '@nhtio/adk/batteries/validation'

const myCustomProfile: OrderingProfile = {
  name: 'custom-internal-guard',
  description: 'Custom internal pipeline constraints.',
  rules: [
    {
      type: 'order',
      id: 'custom-thought-order',
      before: 'thought',
      after: 'toolCall',
      scope: 'adjacent-same-role-group',
    },
  ],
}

const middleware = orderingGuardDispatchMiddleware({
  profiles: ['nova', myCustomProfile],
  action: 'enforce',
})
```

## See Also

* **[Validation Hub](./index)** — Overview and model selector matrix.
* **[Atomic Behaviors](./behaviors)** — Complete catalog of built-in atomic profiles.
* **[Rule Types Reference](./rule-types)** — Specification of the seven rule contracts.
* **[Family Recipes Catalog](./recipes)** — Reference list of all 38 family recipes.
