> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moonbounce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Version hashes

> The exact algorithm behind a version's structure_hash and a question's hash, with test vectors you can check an implementation against.

A decision's version is identified by its **structure hash**. [Versions and hashes](https://docs.moonbounce.ai/learn/versions.md) says what the hash means; this page is the algorithm, precise enough to compute it yourself, for instance to check in CI that a file is the version you think it is, without calling the API. `POST /v1/validate` computes the same thing and returns the text it hashed, so you can compare byte for byte.

A hash is a number-free name: the same structure gets the same hash on your laptop, in CI and in production. A version also has a **number** (1, 2, 3, in the order the structure was registered under its decision), but that number is for people. It is not part of the hash and two decisions that register the same structure number it independently.

## The recipe

1. Read the structure as the API does, which trims some strings ([Whitespace trimmed when a structure is read](#whitespace-trimmed-when-a-structure-is-read)), then build its **preimage**, an object with five keys (below), after the canonicalization rules.
2. Serialize it as canonical JSON with the JSON Canonicalization Scheme, [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) (JCS): no whitespace, object keys sorted by their UTF-16 code units, strings and numbers written the way ECMAScript writes them.
3. Encode the text as UTF-8 and take its SHA-256.
4. Write the digest as lowercase hexadecimal and put `sha256:` in front: `sha256:` plus 64 characters.

## What is hashed

The **structure hash** is the hash of this object, where each value is the canonical form of what you wrote:

```json theme={null}
{
  "format": "moonbounce.structure/1",
  "questions": { "<name>": { "...": "canonical question" } },
  "attributes": { "<name>": { "...": "canonical attribute" } },
  "aggregations": { "<name>": { "...": "canonical aggregation" } },
  "result": "aggregations.handling"
}
```

All five keys are always present: a structure with no attributes or aggregations hashes `"attributes": {}` and `"aggregations": {}`. Names, and the paths in `reads`, `result` and aggregation inputs, are hashed as written. Nothing outside the structure is hashed: not the `decision_id`, not the `objective`, not the state or attribute values you send, not the model that answers.

A **question hash** is the hash of a smaller object, built from one question and not its name:

```json theme={null}
{
  "format": "moonbounce.structure/1",
  "question": { "...": "canonical question" }
}
```

So renaming a question changes the structure hash and leaves that question's hash alone, while rewording it changes both. The `question_hashes` that `POST /v1/validate` returns map each question's name to its hash, and feedback about a question is stored against that hash.

The format string is part of the preimage, so a future change to these rules would be a new format (`moonbounce.structure/2`), and every existing hash would still resolve.

## Whitespace trimmed when a structure is read

Before any canonicalization rule, the API reads the structure with its schema, and the schema trims leading and trailing whitespace from three kinds of string. Trimming is ECMAScript's `String.prototype.trim`: it removes every WhiteSpace and LineTerminator character, which includes the no-break space (U+00A0), the byte order mark (U+FEFF) and the other Unicode space separators as well as spaces, tabs and newlines; a port that uses its own language's strip function has to match that set. An implementation has to do the same first, or a padded string hashes differently from what the API computes:

* a question's `instructions` when it is a string;
* the values of a question's `criteria` (`true` and `false`);
* an attribute's `source`.

Nothing else is trimmed. When `instructions` is an object or an array (`{context, question}`, for instance), the strings inside it are hashed as written, with their whitespace; so are names, paths, `enum` labels, `default`, outcomes and every other string. A string that is empty after trimming is invalid, a 400, not a hash. The vectors `trimmed-padded` and `nested-instructions-padded` show both sides.

## Canonicalization rules

The rules run before step 2, in this order, on the structure as read. A structure that differs from another only in what these rules erase has the same hash.

**Questions**

1. Every string value inside a question is normalized to Unicode NFC: `type`, `instructions` (whether a string or `{context, question}`) and the values of `criteria`. Object keys are not normalized, and neither are the paths in `reads`, which select keys in your content.
2. An empty `criteria` (`{}`) is removed.
3. `reads` is sorted, by UTF-16 code units, with each path as written.

**Attributes**

4. A string attribute's `enum` is sorted. Its labels and `default` stay exactly as written, since they are compared with values in your content.
5. An attribute's `source` is normalized to NFC. Number and boolean attributes are otherwise unchanged.

**Aggregations**

6. On every input of an aggregation and every comparison in a rule's condition, `invert: false` is removed, and so is an explicit `gte: 0.5` (the default threshold). Other thresholds (`gt`, `lte`, `lt`, any other `gte`) stay.
7. On a `weighted_mean`, if every input has weight 1 or no weight, the weights are removed, so writing `weight: 1` everywhere hashes like writing none. A `logistic` always keeps its weights.
8. An aggregation's `inputs` are sorted by the canonical JSON text of each input, compared by UTF-16 code units. Their order never counts.

**What keeps its order**

9. `rules` keep their order, because the first rule that is true wins; so do the operands of `all`, `any` and `not` inside a condition. A `bands` aggregation's `cuts` keep their order too (validation requires them ascending by `at`, so there is only one valid order). Everything else in an aggregation, such as `bias`, `below`, `abstain`, `else` and `cases`, is hashed as written.

Floating-point numbers are written by JCS in their shortest ECMAScript form, so `0.50` and `0.5` are the same number and hash the same.

## Check a hash with the API

`POST /v1/validate` needs no key and stores nothing. Besides the hashes it returns `canonical`, the exact JCS text the structure hash is the SHA-256 of, as a string. When your implementation disagrees with the API, compare your text with this one first:

```bash theme={null}
curl -sS -X POST https://api.moonbounce.ai/v1/validate \
  -H 'Content-Type: application/json' \
  -d '{"structure":{"questions":{"toxic":{"type":"noul","instructions":"Is the text abusive or hateful?"}},"result":"questions.toxic"}}' \
  | jq -r .canonical | tr -d '\n' | sha256sum
```

The digest it prints is the 64 characters after `sha256:` in the response's `structure_hash`. `mb validate decision.json` prints the hash from the command line, and `mb validate --local decision.json` computes it without calling the API.

## Test vectors

Each vector below is a structure and the hash an implementation must produce. Vectors that say "the same as" differ from their partner in exactly the way the rule describes. The vectors live in `packages/spec/test/fixtures/hash-vectors.json` in the Moonbounce repository, which holds each structure as written (the raw input, before it is read and trimmed), its canonical text, its structure hash and its question hashes. A test asserts every one against the code and checks that this page prints them, so the page and the fixture cannot drift apart.

#### `minimal`

The smallest structure: one question that is the result.

```json theme={null}
{"questions":{"toxic":{"type":"noul","instructions":"Is the text abusive or hateful?"}},"result":"questions.toxic"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"toxic":{"instructions":"Is the text abusive or hateful?","type":"noul"}},"result":"questions.toxic"}
```

Structure hash:

```text theme={null}
sha256:81920c187534a1b85305f0bcb99ea866a523e74a9d347f5bd1db24b4a11cf6d6
```

#### `key-order`

Key order and formatting never count: the same structure as `minimal`, with every object's keys in another order.

```json theme={null}
{"result":"questions.toxic","questions":{"toxic":{"instructions":"Is the text abusive or hateful?","type":"noul"}}}
```

Structure hash, the same as `minimal`:

```text theme={null}
sha256:81920c187534a1b85305f0bcb99ea866a523e74a9d347f5bd1db24b4a11cf6d6
```

#### `reads-sorted`

A question's `reads` are sorted, so their order never counts.

```json theme={null}
{"questions":{"late":{"type":"noul","instructions":"Is the request outside the return window?","reads":["state.order.delivered_at","state.message"]}},"result":"questions.late"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"late":{"instructions":"Is the request outside the return window?","reads":["state.message","state.order.delivered_at"],"type":"noul"}},"result":"questions.late"}
```

Structure hash:

```text theme={null}
sha256:a3f37e31d804a9c581c67e74e95fa15cd47619ca5fd444b9e6f75b257e132a94
```

#### `reads-reordered`

The same question with `reads` in the other order.

```json theme={null}
{"questions":{"late":{"type":"noul","instructions":"Is the request outside the return window?","reads":["state.message","state.order.delivered_at"]}},"result":"questions.late"}
```

Structure hash, the same as `reads-sorted`:

```text theme={null}
sha256:a3f37e31d804a9c581c67e74e95fa15cd47619ca5fd444b9e6f75b257e132a94
```

#### `inputs-sorted`

An aggregation's `inputs` are sorted, so their order never counts. Here `max` lists its inputs in the reverse of the sorted order.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"any_of":{"type":"max","inputs":[{"name":"questions.b"},{"name":"questions.a"}]}},"result":"aggregations.any_of"}
```

Canonical text:

```text theme={null}
{"aggregations":{"any_of":{"inputs":[{"name":"questions.a"},{"name":"questions.b"}],"type":"max"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.any_of"}
```

Structure hash:

```text theme={null}
sha256:d4b2eed3d2720b9f6ed2ced2ea908c5da5457ea6c527848816b252ee56e46602
```

#### `inputs-reordered`

The same aggregation with its inputs in sorted order.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"any_of":{"type":"max","inputs":[{"name":"questions.a"},{"name":"questions.b"}]}},"result":"aggregations.any_of"}
```

Structure hash, the same as `inputs-sorted`:

```text theme={null}
sha256:d4b2eed3d2720b9f6ed2ced2ea908c5da5457ea6c527848816b252ee56e46602
```

#### `enum-sorted`

A string attribute's `enum` is sorted; the labels themselves stay exact.

```json theme={null}
{"questions":{"q":{"type":"noul","instructions":"Q?"}},"attributes":{"tier":{"type":"string","enum":["new","gold","basic"],"default":"new"}},"aggregations":{"by_tier":{"type":"match","input":"attributes.tier","cases":{"gold":"vip","new":"standard","basic":"standard"}}},"result":"aggregations.by_tier"}
```

Canonical text:

```text theme={null}
{"aggregations":{"by_tier":{"cases":{"basic":"standard","gold":"vip","new":"standard"},"input":"attributes.tier","type":"match"}},"attributes":{"tier":{"default":"new","enum":["basic","gold","new"],"type":"string"}},"format":"moonbounce.structure/1","questions":{"q":{"instructions":"Q?","type":"noul"}},"result":"aggregations.by_tier"}
```

Structure hash:

```text theme={null}
sha256:786519a3deb38f165266c84a8f8dcc5052eaefe5ee4a313d84f1e6fcceb51083
```

#### `enum-reordered`

The same attribute with its `enum` already sorted.

```json theme={null}
{"questions":{"q":{"type":"noul","instructions":"Q?"}},"attributes":{"tier":{"type":"string","enum":["basic","gold","new"],"default":"new"}},"aggregations":{"by_tier":{"type":"match","input":"attributes.tier","cases":{"gold":"vip","new":"standard","basic":"standard"}}},"result":"aggregations.by_tier"}
```

Structure hash, the same as `enum-sorted`:

```text theme={null}
sha256:786519a3deb38f165266c84a8f8dcc5052eaefe5ee4a313d84f1e6fcceb51083
```

#### `default-invert-false`

`invert: false` equals the default and is stripped from an input.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"both":{"type":"min","inputs":[{"name":"questions.a","invert":false},{"name":"questions.b"}]}},"result":"aggregations.both"}
```

Structure hash, the same as `default-bare`:

```text theme={null}
sha256:749a4b636e945bde12eb26ae4152e021a2b18594710a6702f77c5fb557f2d924
```

#### `default-bare`

The same aggregation with no `invert`.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"both":{"type":"min","inputs":[{"name":"questions.a"},{"name":"questions.b"}]}},"result":"aggregations.both"}
```

Canonical text:

```text theme={null}
{"aggregations":{"both":{"inputs":[{"name":"questions.a"},{"name":"questions.b"}],"type":"min"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.both"}
```

Structure hash:

```text theme={null}
sha256:749a4b636e945bde12eb26ae4152e021a2b18594710a6702f77c5fb557f2d924
```

#### `default-gte-half`

An explicit `gte: 0.5`, the default threshold of `all` and `any`, is stripped.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"every":{"type":"all","inputs":[{"name":"questions.a","gte":0.5},{"name":"questions.b"}]}},"result":"aggregations.every"}
```

Structure hash, the same as `default-threshold-bare`:

```text theme={null}
sha256:4cd0045f924b8ab517410583c85780d09d06338f40a7f8feafb76b6765f9a7b8
```

#### `default-threshold-bare`

The same check with no threshold.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"every":{"type":"all","inputs":[{"name":"questions.a"},{"name":"questions.b"}]}},"result":"aggregations.every"}
```

Canonical text:

```text theme={null}
{"aggregations":{"every":{"inputs":[{"name":"questions.a"},{"name":"questions.b"}],"type":"all"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.every"}
```

Structure hash:

```text theme={null}
sha256:4cd0045f924b8ab517410583c85780d09d06338f40a7f8feafb76b6765f9a7b8
```

#### `threshold-kept`

Any other threshold is content: `gte: 0.7` changes the hash.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"every":{"type":"all","inputs":[{"name":"questions.a","gte":0.7},{"name":"questions.b"}]}},"result":"aggregations.every"}
```

Canonical text:

```text theme={null}
{"aggregations":{"every":{"inputs":[{"gte":0.7,"name":"questions.a"},{"name":"questions.b"}],"type":"all"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.every"}
```

Structure hash:

```text theme={null}
sha256:3c93e2d43d533c1df28bee0b0e16a4c756813ea2e6e1edf0ca30cc1e588a4a21
```

This is not the hash of `default-threshold-bare`.

#### `empty-criteria`

Empty `criteria` (`{}`) is stripped from a question.

```json theme={null}
{"questions":{"angry":{"type":"noul","instructions":"Is the customer angry?","criteria":{}}},"result":"questions.angry"}
```

Structure hash, the same as `minimal-angry`:

```text theme={null}
sha256:2b8ca571a6d33cee3b428cc9028a5bdd62ba1d29d6bb90e3409570a0fb3305c3
```

#### `minimal-angry`

The same question with no `criteria`.

```json theme={null}
{"questions":{"angry":{"type":"noul","instructions":"Is the customer angry?"}},"result":"questions.angry"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"angry":{"instructions":"Is the customer angry?","type":"noul"}},"result":"questions.angry"}
```

Structure hash:

```text theme={null}
sha256:2b8ca571a6d33cee3b428cc9028a5bdd62ba1d29d6bb90e3409570a0fb3305c3
```

#### `all-1-weights`

On a `weighted_mean`, weights that are all 1 hash like no weights at all.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"mean":{"type":"weighted_mean","inputs":[{"name":"questions.a","weight":1},{"name":"questions.b","weight":1}]}},"result":"aggregations.mean"}
```

Structure hash, the same as `no-weights`:

```text theme={null}
sha256:b32a32b5adbbcabfafc93cf9367070613154f18c56087b6a614177d8c5541c53
```

#### `no-weights`

The same mean with no weights.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"mean":{"type":"weighted_mean","inputs":[{"name":"questions.a"},{"name":"questions.b"}]}},"result":"aggregations.mean"}
```

Canonical text:

```text theme={null}
{"aggregations":{"mean":{"inputs":[{"name":"questions.a"},{"name":"questions.b"}],"type":"weighted_mean"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.mean"}
```

Structure hash:

```text theme={null}
sha256:b32a32b5adbbcabfafc93cf9367070613154f18c56087b6a614177d8c5541c53
```

#### `weights-kept`

Any other weight is content: a 2 on one input changes the hash.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"mean":{"type":"weighted_mean","inputs":[{"name":"questions.a","weight":2},{"name":"questions.b","weight":1}]}},"result":"aggregations.mean"}
```

Canonical text:

```text theme={null}
{"aggregations":{"mean":{"inputs":[{"name":"questions.a","weight":2},{"name":"questions.b","weight":1}],"type":"weighted_mean"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.mean"}
```

Structure hash:

```text theme={null}
sha256:e72eec49aa9c05a00afd772636ecc3f37ec1caf2b8ae39ffbfa85d2da85ce88d
```

This is not the hash of `no-weights`.

#### `nfc-decomposed`

Instruction text is Unicode-normalized (NFC): `e` followed by U+0301 hashes like `é` (U+00E9).

```json theme={null}
{"questions":{"q":{"type":"noul","instructions":"Is the cafe\u0301 open?"}},"result":"questions.q"}
```

Structure hash, the same as `nfc-composed`:

```text theme={null}
sha256:8644c6ea7a5b5369472b3baea35921b60f6b00a7ef5d2c5d65f346d3fafade7f
```

#### `nfc-composed`

The same instruction with the composed character.

```json theme={null}
{"questions":{"q":{"type":"noul","instructions":"Is the caf\u00e9 open?"}},"result":"questions.q"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"q":{"instructions":"Is the café open?","type":"noul"}},"result":"questions.q"}
```

Structure hash:

```text theme={null}
sha256:8644c6ea7a5b5369472b3baea35921b60f6b00a7ef5d2c5d65f346d3fafade7f
```

#### `trimmed`

Strings the schema trims, already without surrounding whitespace: a string `instructions`, a `criteria` value and an attribute's `source`.

```json theme={null}
{"questions":{"used":{"type":"noul","instructions":"Has the item been used?","criteria":{"true":"Signs of wear or a broken seal."}}},"attributes":{"fraud_score":{"type":"number","source":"fraud-model-v2"}},"aggregations":{"route":{"type":"bands","input":"questions.used","below":"refund","cuts":[{"at":0.5,"outcome":"review"}]}},"result":"aggregations.route"}
```

Canonical text:

```text theme={null}
{"aggregations":{"route":{"below":"refund","cuts":[{"at":0.5,"outcome":"review"}],"input":"questions.used","type":"bands"}},"attributes":{"fraud_score":{"source":"fraud-model-v2","type":"number"}},"format":"moonbounce.structure/1","questions":{"used":{"criteria":{"true":"Signs of wear or a broken seal."},"instructions":"Has the item been used?","type":"noul"}},"result":"aggregations.route"}
```

Structure hash:

```text theme={null}
sha256:57e3f04a3ec26460f6b6857cba313c9d2bc3d545bb5ee4d0b395828be00dd01b
```

#### `trimmed-padded`

The same structure with spaces and a newline around those strings. Reading the structure trims them, before any canonicalization rule runs, so the hash is the same.

```json theme={null}
{"questions":{"used":{"type":"noul","instructions":"  Has the item been used?\n","criteria":{"true":" Signs of wear or a broken seal.  "}}},"attributes":{"fraud_score":{"type":"number","source":" fraud-model-v2 "}},"aggregations":{"route":{"type":"bands","input":"questions.used","below":"refund","cuts":[{"at":0.5,"outcome":"review"}]}},"result":"aggregations.route"}
```

Structure hash, the same as `trimmed`:

```text theme={null}
sha256:57e3f04a3ec26460f6b6857cba313c9d2bc3d545bb5ee4d0b395828be00dd01b
```

#### `nested-instructions`

`instructions` as an object: its strings are hashed as written (after NFC), not trimmed.

```json theme={null}
{"questions":{"refund":{"type":"noul","instructions":{"context":"Support inbox","question":"Should this be refunded?"}}},"result":"questions.refund"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"refund":{"instructions":{"context":"Support inbox","question":"Should this be refunded?"},"type":"noul"}},"result":"questions.refund"}
```

Structure hash:

```text theme={null}
sha256:2bee49e68a94937f1f53c001136d4a859b5b6c833d7166cb2a9a28d6bf7aade7
```

#### `nested-instructions-padded`

The same object with spaces around its strings. Strings inside object or array instructions are not trimmed, so this is a different hash.

```json theme={null}
{"questions":{"refund":{"type":"noul","instructions":{"context":" Support inbox","question":"Should this be refunded? "}}},"result":"questions.refund"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"refund":{"instructions":{"context":" Support inbox","question":"Should this be refunded? "},"type":"noul"}},"result":"questions.refund"}
```

Structure hash:

```text theme={null}
sha256:cc699c9dc2f33900cddab6d62011a3435f397977e7dc556ce402910da5098d8e
```

This is not the hash of `nested-instructions`.

#### `rules-in-order`

The order of `rules` is kept, because the first rule that is true wins.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"route":{"type":"rules","rules":[{"when":{"name":"questions.a"},"outcome":"escalate"},{"when":{"name":"questions.b"},"outcome":"grant"}],"else":"deny"}},"result":"aggregations.route"}
```

Canonical text:

```text theme={null}
{"aggregations":{"route":{"else":"deny","rules":[{"outcome":"escalate","when":{"name":"questions.a"}},{"outcome":"grant","when":{"name":"questions.b"}}],"type":"rules"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.route"}
```

Structure hash:

```text theme={null}
sha256:c8036ec91ad0a777da77713b55ff71d420a3f7a4ba1b2d5192a09f972c0a13c6
```

#### `rules-swapped`

The same rules in the other order are another structure.

```json theme={null}
{"questions":{"a":{"type":"noul","instructions":"A?"},"b":{"type":"noul","instructions":"B?"}},"aggregations":{"route":{"type":"rules","rules":[{"when":{"name":"questions.b"},"outcome":"grant"},{"when":{"name":"questions.a"},"outcome":"escalate"}],"else":"deny"}},"result":"aggregations.route"}
```

Canonical text:

```text theme={null}
{"aggregations":{"route":{"else":"deny","rules":[{"outcome":"grant","when":{"name":"questions.b"}},{"outcome":"escalate","when":{"name":"questions.a"}}],"type":"rules"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"a":{"instructions":"A?","type":"noul"},"b":{"instructions":"B?","type":"noul"}},"result":"aggregations.route"}
```

Structure hash:

```text theme={null}
sha256:8fab15facf8fb577d53e4593ae7656ec6760eeff74c817f3d7c26a5ccc2f3cc1
```

This is not the hash of `rules-in-order`.

#### `bands-cuts`

A `bands` aggregation's `cuts` keep their order (ascending by `at`, which validation requires).

```json theme={null}
{"questions":{"risk":{"type":"noul","instructions":"Is the order risky?"}},"aggregations":{"handling":{"type":"bands","input":"questions.risk","below":"auto_reply","cuts":[{"at":0.4,"outcome":"review"},{"at":0.8,"outcome":"escalate"}],"abstain":"escalate"}},"result":"aggregations.handling"}
```

Canonical text:

```text theme={null}
{"aggregations":{"handling":{"abstain":"escalate","below":"auto_reply","cuts":[{"at":0.4,"outcome":"review"},{"at":0.8,"outcome":"escalate"}],"input":"questions.risk","type":"bands"}},"attributes":{},"format":"moonbounce.structure/1","questions":{"risk":{"instructions":"Is the order risky?","type":"noul"}},"result":"aggregations.handling"}
```

Structure hash:

```text theme={null}
sha256:7484f4579a651097cdadf54c1db82aaad8c30b9c2cad5b4a6c26480703b8b38d
```

#### `rename-question`

A question's name is part of the structure but not of the question's own hash: renaming `toxic` changes `structure_hash`, not that question's hash.

```json theme={null}
{"questions":{"abusive":{"type":"noul","instructions":"Is the text abusive or hateful?"}},"result":"questions.abusive"}
```

Canonical text:

```text theme={null}
{"aggregations":{},"attributes":{},"format":"moonbounce.structure/1","questions":{"abusive":{"instructions":"Is the text abusive or hateful?","type":"noul"}},"result":"questions.abusive"}
```

Structure hash:

```text theme={null}
sha256:647d90f544af91ec29c31acb7705652852a0ccbbad0fad5ff19552d05fb06fc5
```

This is not the hash of `minimal`.

## Next

[Versions and hashes](https://docs.moonbounce.ai/learn/versions.md) covers what changes a hash and how to register a structure. [The glossary](https://docs.moonbounce.ai/learn/glossary.md) defines version, structure and revision.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.