Skip to main content

Blue Language Specification 1.0

PART I — THE LANGUAGE (1–14)

Scope of Part I. Defines Blue’s content model, typing, resolution, schema, identity (BlueId), expansion and minimization. No runtime/contract processing is defined here (that is Part II). Where Part I references types like Text, Integer, List, Boolean, etc., their canonical definitions (with BlueIds) are in Appendix A.

1. Scope & Goals

Goal. Blue is a universal, deterministic content language with:

  • a strict, mergeable type system and resolution rules, and
  • a content address called BlueId that is stable across equivalent authoring forms (minimal, expanded, resolved).

Out of scope. Runtime/workflow semantics, policies, operations, and processors (colloquially “contracts”) are not defined here. They live in Blue Contracts & Processor Model.


2. Core Data Model

A Blue node is exactly one of:

  • Scalar (string, number, boolean)
  • List (array)
  • Object (map of fields → nodes)

Reserved keys (language keywords) that MAY appear in any object node:

name, description, type, itemType, keyType, valueType,
value, items, blueId, blue, schema, mergePolicy, contracts

There is no properties field in the language. An “object” node is written as regular fields under that node. (properties may exist internally in some libraries; it MUST NOT appear in documents.)

2.1 Wrapper Equivalence (normative)

To improve ergonomics, Blue admits two equivalent authoring forms for scalars and lists:

  • Scalar: x: 1 x: { value: 1 } (equivalent only if the wrapper has no other keys)
  • List: x: [a, b] x: { items: [a, b] } (equivalent only if the wrapper has no other keys)

Object nodes are written directly:

x:
a: 1
b: 2

A node MUST NOT combine payload kinds: a node may have either value or items or child fields (object) — never more than one. (Authors MAY rely on preprocessing to normalize forms; see §7.)


3. name / description — Identity vs Field Semantics

3.1 Meaning and neutrality (normative)

  • name and description are content on the node; they do affect BlueId.
  • They are node-local (document identity or field label).
  • They MUST be ignored by:
    • type conformance checks,
    • subtype compatibility checks,
    • structural/shape matchers (including resolution matchers).

(Matcher Neutrality.) Matchers MUST ignore name, description.

3.2 Document identity vs field semantics (normative)

  • Document-level identity. A node “of type T” is not T; it is a new entity. The resolved node’s top-level name/description come only from the instance and MUST NOT be inherited from type. The embedded type object may carry its own name/description inside node.type.
  • Field-level semantics. When a type materializes fields/items into the instance, those child nodes carry the type’s name/description until the instance explicitly overrides them on those child paths.

3.3 Expansion (node references) (normative)

Dereferencing { blueId: X } to materialize the node may copy the referenced node’s name/description onto that node (because the node itself is being materialized). This is not inheritance from type and does not violate §3.2.

3.4 Equality (normative)

  • Identity equality (BlueId) includes name/description.
  • Structural/type equality ignores name/description.

4. Types — Schema and Overlay (Uniform Model)

4.1 Any node can be a type (normative)

There is no “schema vs instance” bifurcation. Any node can appear under type. If T is used in type: T, T contributes:

  • structure (fields/items),
  • nested type chains,
  • schema constraints (§5),
  • fixed values (become invariants).

Fixed-value invariant. A concrete value in a type is immutable in descendants at that path.

4.2 Subtyping & Liskov (normative)

When resolving, descendants MUST satisfy:

  1. No fixed-value override (immutable values).
  2. Type compatibility (equal or subtype of inherited type).
  3. Additive structure (cannot remove guaranteed fields).
  4. Collections maintain itemType, keyType, valueType compatibility.

Liskov. Every instance of a subtype MUST be substitutable for its parent.

4.3 Instance-as-type (overlay)

Nodes representing individuals can be used as types:

  • Alice may be type: Person.
  • Alice Smith may be type: Alice.

All fixed values in Alice become invariants in Alice Smith. Alice’s name/description do not flow to Alice Smith (§3.2).


5. schema — Constraint Keywords (complete list)

Attach schema to any node. All constraints accumulate along the type chain; stricter wins. Irreconcilable constraints MUST fail resolution (§11).

5.1 Presence

  • required: true — the field must be present in resolved descendants.

5.2 For lists

  • minItems: <non-negative integer>
  • maxItems: <non-negative integer> (≥ minItems)
  • uniqueItems: true|false (uniqueness by BlueId of items)

5.3 For objects (maps)

  • minFields: <non-negative integer>
  • maxFields: <non-negative integer> (≥ minFields)

(Rationale: “fields” reflects there is no properties key in the language.)

5.4 Numerics (applies to numeric scalars)

  • minimum: number
  • maximum: number (≥ minimum if both present)
  • exclusiveMinimum: number (strictly less than value)
  • exclusiveMaximum: number (strictly greater than value)
  • multipleOf: number (> 0). If multiple appear in the chain, use LCM.

5.5 Strings

  • minLength: <non-negative integer>
  • maxLength: <non-negative integer> (≥ minLength)
  • pattern: <ECMA-262 regex string>

5.6 Enumerations

  • enum: [ v1, v2, … ] (values are scalars; equality by canonical JSON)

Note. There is no separate const keyword; a fixed value in the type (i.e., the type sets value or a concrete subtree value) already enforces a constant.


6. Resolution Semantics

Goal. Produce a fully validated, resolved snapshot from authoring/overlay input.

6.1 Algorithm (normative)

Given a source node S:

  1. Preprocess blue (§7).
  2. Resolve type chain: if S.type exists, recursively resolve it (following blueId via a provider) to produce ancestor A.
  3. Merge A into target T, then merge S into T:
  • Values: copy if absent; if both present, must be equal.
  • Types: assign/propagate under §4.2.
  • Schema: accumulate (§5).
  • Object fields: merge recursively; child must remain compatible.
  • Lists: see §6.3.
  1. Validate all schema constraints.
  2. Finalize the resolved snapshot (implementations SHOULD freeze it; §10).

6.2 Requirement Overlays (normative)

An ancestor may partially constrain a subtree (overlay obligations) without binding a concrete type at that path:

# Parent
name: A
prop1:
x: 1 # requirement overlay at /prop1 (no type)
schema: { ... } # optional constraints

A descendant may later set prop1.type: Some only if the merged result still satisfies all overlay obligations (fixed values and schema). If the overlay had a type, the descendant’s type must be equal or a subtype.

Conflicts (e.g., overlay forces x = 1 but Some forces x = 2) MUST fail resolution.

6.3 Lists at authoring time (normative)

  • Refinement of an inherited index is allowed if the element remains subtype-compatible with the inherited element.
  • Append is allowed.
  • Deletion/reorder within the inherited prefix MUST fail resolution.

6.4 Limits (normative)

Resolvers SHOULD support path/depth limits to bound expansion of large graphs. Limits affect only materialization, not meaning.


7. Preprocessing — the blue Directive (normative)

The root MAY contain blue (string or object). Preprocessing:

  • runs before hashing & resolution,
  • normalizes authoring (type aliases → blueId, type inference, renamings),
  • MUST remove the blue directive afterward (it never affects BlueId).

7.1 Required baseline transforms

  1. Type aliases → blueId (per configured mappings).
  2. Primitive inference for bare scalars (assign Text, Integer, Double, Boolean).
  3. Optional attribute/case/date normalizations (profile-dependent).

7.2 Security

Remote fetch of transforms is DISABLED by default (opt-in only).


8. BlueId (Content Address)

8.1 Definition (normative)

BlueId = Base58( SHA-256( RFC 8785-canonical-JSON(node) ) ) computed bottom-up (children before parents).

8.2 Cleaning & Shape Normalization (normative)

Before producing RFC 8785 canonical JSON, implementations MUST normalize nodes as follows. The aims are to remove non-meaningful artifacts, ensure a single canonical shape, and preserve identity-relevant content.

8.2.1 Global cleaning

  1. Remove null at any depth.• In lists, null elements are removed (i.e., [a, null, b] → [a, b]).• In objects, fields with null values are removed.
  2. Remove empty maps {} at any depth (including within lists).• Cascading removal is permitted (a parent map may become {} and then be removed itself).
  3. Preserve empty lists []. A present-empty list is not the same as an absent field.

Note: This “cleaning” is applied recursively across the entire tree before hashing.

8.2.2 Canonical shape: wrapped form is official

Blue’s official canonical representation is the wrapped form. Authoring sugar MUST be normalized to wrapped form prior to hashing:

  • ScalarsAuthoring sugar:
x: 1
  • Canonical wrapped form:
x:
value: 1
  • ListsAuthoring sugar:
x: [a, b]
  • Canonical wrapped form:
x:
items: [a, b]

A node MUST NOT combine payload kinds (value, items, or object fields). After normalization, each node is exactly one of:

  • scalar wrapper { value: … },
  • list wrapper { items: […] }, or
  • plain object (map of fields → nodes).

Equivalence guarantee: Even if an implementation hashes authoring sugar directly, the hasher MUST treat x: 1 and x: { value: 1 } (and likewise x: [a,b] vs x: { items: [a,b] }) as identical (§ 8.3, § 8.4 ensure this by inlining value and domain-separating list folds).

8.2.3 List control forms (see § 12)

Reserved list item forms are not content (except $empty) and MUST be handled specially before hashing:

  • $pos overlaysconsumed by normalization/merge; they do not appear in the hashed content list.
  • $previous anchors — never content. Hashers either:(a) seed the list fold with the provided prior list BlueId iff the resolved prefix is unchanged (§ 12.5.2), or(b) recompute from id([]) (§ 8.4, § 12.5.1) if the prefix changed or no valid anchor is present.
  • $empty: trueis content and remains as a real element; it hashes like any other object element (§ 12.4).

These forms are recognized only at the top level of items for nodes whose type is List.

8.2.4 Object shape

After normalization, an object node is a plain map of field → node.

There is no properties field in the language; that is an internal detail in some libraries and MUST NOT appear in documents. Key order is irrelevant (RFC 8785 canonicalization defines ordering).

8.2.5 Examples (informative)

  • Nulls inside a list (removed):
list: [a, null, b]

→ canonical wrapped content:

list:
items: [a, b]
  • Scalar sugar normalized:
x: 1

x:
value: 1
  • List sugar normalized:
x: [a, b]

x:
items: [a, b]
  • $empty is content (preserved):
list:
items: [a, { $empty: true }, b]
  • $pos consumed before hashing:
list:
items:
- $pos: 0
value: a'
- b

→ normalized content before hashing:

list:
items: [a', b]
  • $previous as anchor (not content):
list:
items:
- $previous: { blueId: P }
- c
  • → if prefix truly matches P, hasher seeds fold with P and folds only c; otherwise recomputes from id([]).

8.3 Map hashing (normative)

If and only if the map is exactly { "blueId": "<id>" }, return <id> (pure reference short-circuit).

Otherwise, build a helper map H in lexicographic key order:

  • For name, description, and value: inline their values.
  • For every other key k with value v: include k: { "blueId": id(v) }.

Then RFC 8785-canonicalize H and hash.

This rule ensures scalar wrappers { value: S } are treated as scalars for hashing, and that all nested structure contributes by BlueId, not by byte shape.

8.4 List hashing (normative)

Use a domain-separated streaming fold over element BlueIds:

  • Empty list seed:
id([]) = H({ "$list": "empty" })
  • Fold step:
fold(prevId, x) =
H({
"$listCons": {
"prev": { "blueId": prevId },
"elem": { "blueId": id(x) }
}
})
  • Whole list:
id([a₁,…,aₙ]) = fold( fold( … fold( id([]), a₁ ) … , aₙ ) )

Properties:

  • Order is significant; multiplicity is preserved.
  • No flattening ([[1,2],3] ≠ [1,2,3]).
  • Singleton is distinct ([A] ≠ A).
  • Appends can be O(Δ) when seeded by a valid $previous (§ 12.5.2).

Seeding with $previous: If the final resolved list’s prefix exactly equals the list whose BlueId is provided by $previous, start the fold from that BlueId and only fold appended elements. Otherwise ignore the anchor and recompute from id([]).

8.5 Scalars (normative)

Hash the RFC 8785 canonical JSON representation of the scalar value.

8.6 Storage rule (normative)

A node MUST NOT store its own BlueId as authoritative content. Using { "blueId": "…" } to reference other nodes is permitted and encouraged.


9. Expansion

Expansion materializes content referenced by blueId without changing identity. It is distinct from Resolution (§6).

9.1 What expansion does (normative)

Given p: { blueId: X }, expansion fetches content for X and materializes it in place (or side-by-side) within limits (§9.3), allowing nested references to expand recursively.

9.2 Identity invariance (normative)

Expansion MUST NOT change BlueId. Pure references hash to their blueId (short-circuit), and materialized subtrees contribute the same id(subtree) because map hashing replaces structure with { blueId: … } under the hood.

9.3 Limits (normative)

Implementations SHOULD support path/depth limits to avoid runaway traversal of large graphs. Limits affect only materialization, not identity.


10. Minimization (Normalizing Back to Authoring Form)

Minimization produces a minimal authoring view that, when re-resolved, yields the same resolved snapshot and the same BlueId.

10.1 Rules (normative)

Given a resolved snapshot R, minimize(R) MUST:

  • Remove any fields fully re-derivable from the type chain (structure brought solely by types, defaulted values that are fixed by types).
  • Replace materialized types with their canonical type: { blueId: … } forms when available.
  • Optionally collapse large subtrees to { blueId: … } when the subtree equals a known blueId (this is an allowed minimization, not required).
  • Preserve instance-level name/description on the node.
  • Never remove instance-fixed values that are not derivable from the type chain.

11. Circular References (Combined BlueId for Direct Cycles)

Some authoring graphs contain direct cycles across documents (e.g., Person ↔ Dog). Blue supports a combined BlueIdwith per-document suffixes.

11.1 Authoring placeholders (normative)

During save/serialization of a cyclic set:

  • Temporarily replace each direct cyclic blueId reference with a placeholder 0×44 (forty-four ASCII '0' characters) — ZERO_BLUEID sentinel that never appears as a valid BlueId.
  • Calculate preliminary BlueIds for each document in isolation (with placeholders).
  • Sort documents lexicographically by their preliminary BlueIds and assign positions #0..#(n-1).

Rewrite placeholders:

  • For each internal cyclic reference that points to another document in the set, place blueId: "this#k" where k is the assigned position of the target.

11.2 Master & final BlueIds (normative)

  • Build a list L = [doc#0, doc#1, …, doc#(n-1)] with the this#k references in place and compute its BlueId → MASTER.
  • Each document’s final BlueId becomes: MASTER#i where i is the document’s position.

These final BlueIds are stable, content-addressed identities for the cyclic set.

Example.

# Person (#1 before sorting)
name: Person
pet:
type: { blueId: 'this#1' }

# Dog (#0 before sorting)
name: Dog
owner:
type: { blueId: 'this#0' }
breed:
type: Text

If MASTER = "12345...", the final identities are Dog = "12345...#0", Person = "12345...#1".


§12 — Lists (authoring, merge, hashing)

12.1 Authoring model

A list field SHOULD be authored in typed form:

<field>:
type: List
itemType: <Type> # RECOMMENDED
mergePolicy: append-only | positional
items:
- ...elements...

A surface list (e.g., tags: [a, b, c]) is permitted for simple cases; the typed form is REQUIRED when mergePolicy, anchors, or overlays are used.

12.2 Allowed element forms inside items

Each item MUST be exactly one of:

  1. Normal element (content)
- <scalar | object (optionally with type) | { blueId: "…" }>
  1. Append anchor (fast append / immutability proof — reserved)
- $previous:
blueId: <PrevListBlueId>
  • Only allowed as the first item.
  • Shape MUST be exactly as shown (single top-level $previous key whose value is an object with a single blueIdstring).
  1. Positional overlay (refine by index — reserved; mergePolicy: positional only)

Map overlay (best for object elements):

- $pos: 1
...overlay fields merged into parent element at index 1...

Scalar/list overlay (when overlay isn’t a map):

- $pos: 1
value: <scalar | list | { blueId: "…" }>

Constraints:

  • $pos MUST be a non-negative integer (0-based).
  • Only valid when mergePolicy: positional.
  1. Placeholder element (reserved; content)
- $empty: true
  • A real element that occupies a position (a meaningful “hole”).
  • Distinct from null (ignored) and from [] (present-empty list at that node).

Scope of reserved attributes. The special keys $previous, $pos, $empty are recognized only as top-level keys of items within items of a node whose type: List. They have no special meaning elsewhere.

12.3 Default policy

If mergePolicy is omitted, processors MUST assume positional. For histories/ledgers/timelines, authors MUST specify append-only explicitly.

12.4 Semantics of null, {}, [], and $empty

  • null — ignored (no information).
  • Empty object {} — ignored.
  • Empty list [] — preserved (present-empty ≠ absent) and hashes differently from either null or absent.
  • $empty: true — content; remains for hashing.

12.5 Merge semantics

Let parent list be the resolved list from the parent type/instance; let child overlay be the child’s items.

12.5.1 append-only

  • Disallow any modification/removal of indices < parentLength. No $pos allowed.
  • Allow appends (normal items after the parent prefix).
  • Optional append anchor (first item only):
- $previous: { blueId: <PrevListBlueId> }
  • Errors:
    • Any $pos overlay in an append-only list.
    • $previous not first, malformed, or repeated.

12.5.2 positional

  • Refine any index i (0 ≤ i < parentLength) via $pos: i:
    • Map overlay: field-wise merge, subject to type/constraint compatibility.
    • Scalar/list overlay: replace with value, subject to compatibility.
  • Append new elements by listing normal items after overlays (no $pos on appends).
  • Forbid reordering/removal/gaps within the inherited prefix.
  • Errors:
    • $pos missing or non-integer; out of range; duplicate overlays for the same i.
    • Type/constraint incompatibility at i.
    • Attempted reordering/removal of parent elements.

12.6 BlueId calculation for lists

The list hasher uses a domain-separated streaming fold:

  • id([]) = H({ "$list": "empty" })
  • For list [e1, e2, …, en]:
    • acc := id([])
    • For each element e in order:
acc := H({
"$listCons": {
"prev": { "blueId": acc },
"elem": { "blueId": id(e) }
}
})
    • Return acc.

Control forms:

  • $pos overlays are consumed by merge/normalization before hashing (not content).
  • $previous can be used as an optimization: if and only if the effective prefix exactly equals the prior list with BlueId P = $previous.blueId, hashers MAY seed acc := P and fold only the appended elements. If the prefix differs, seed from id([]) and recompute fully.
  • $empty: true is content and hashes like any other element (it is an object node with a boolean field).

Properties guaranteed:

  • Order & multiplicity preserved.
  • No flattening: [[A,B],C] ≠ [A,B,C].
  • Singleton preserved: [A] ≠ A.
  • [] hashes differently from null/absent.
  • O(Δ) appends are possible when $previous is valid.

12.7 Conformance checklist (must-pass)

  • id([]) defined and id([]) ≠ id(null/absent).
  • [A] hashes differently from A.
  • [[A,B],C] hashes differently from [A,B,C].
  • $previous recognized only as first item; ignored when prefix changed.
  • append-only: $pos causes error; normal appends succeed.
  • positional: $pos applies; duplicates/out-of-range cause error; appended items appear after parent prefix in author order.
  • $empty: true remains as content and affects BlueId.
  • Cleaning removes null/{} but not [].

12.8 Worked examples (informative)

Present-empty vs absent

# Absent
doc: { }

# Present-empty
doc:
list:
type: List
items: []

blueId(present-empty) != blueId(absent).

Append-only timeline (fast append, immutable prefix)

# Parent
entries:
type: List
itemType: Timeline Entry
mergePolicy: append-only
items:
- { type: Timeline Entry, ts: 2025-09-01T12:00:00Z, message: A }
- { type: Timeline Entry, ts: 2025-09-01T12:05:00Z, message: B }

# Child (append C)
entries:
type: List
itemType: Timeline Entry
mergePolicy: append-only
items:
- $previous: { blueId: PrevId } # PrevId = id(parent entries)
- { type: Timeline Entry, ts: 2025-09-01T12:10:00Z, message: C }

Hasher seeds from PrevId and folds only the new element(s).

Positional hole and refinement

# Parent
entries:
type: List
mergePolicy: positional
items:
- A
- $empty: true
- C

# Child
entries:
type: List
mergePolicy: positional
items:
- $pos: 1
value: B
# Resolved: [A, B, C]

Positional refine + append

# Parent
items:
type: List
itemType: PhoneOrAccessory
mergePolicy: positional
items:
- { type: Phone, os: any }
- { type: Accessory }

# Child
items:
type: List
mergePolicy: positional
items:
- $pos: 0
type: iPhone
os: iOS
- { type: Warranty, months: 24 }

# Resolved:
# - { type: iPhone, os: iOS }
# - { type: Accessory }
# - { type: Warranty, months: 24 }

13. Conformance

A Blue Language 1.0 implementation:

  • MUST implement preprocessing, resolution, schema validation (Sections 5–7, 11).
  • MUST implement BlueId exactly (Section 8), including wrapper normalization (§8.2).
  • MUST treat name/description as neutral for structural/type checks (§3).
  • SHOULD provide Expansion with limits (Section 9).
  • SHOULD provide Minimization (Section 10).
  • SHOULD expose frozen resolved snapshots (implementation guidance in §10).

Profiles (recommended):

  • BlueId-Only — §8.
  • Resolver-Core — §§5–7, 8, 11.
  • Language-Full — §§3–12.

14. Worked Examples (informative)

14.1 Content-addressable types (Alice / Simple Amount)

name: Simple Amount
amount: { type: Double }
currency:{ type: Text }
# => blueId: FgHZjS...

name: Person
age: { type: Integer }
spent: { type: { blueId: FgHZjS... } } # Simple Amount
# => blueId: GRwTYs...

Instance:

name: Alice
type: { blueId: GRwTYs... } # Person
age: 25
spent:
amount: 27.15
currency: USD
# => blueId: 3JTd8s...

Fully expanded (type chain materialized) has the same BlueId (Expansion & Resolution do not change identity).

14.2 Blue directive (aliases & inference)

blue: 5j04jf...
name: Alice
type: Person
age: 25
spent:
amount: 27.15
currency: USD

Preprocessing replaces Person with its blueId and may infer basic types for scalars. The blue key is removed before hashing.

14.3 “Same image, different meaning”

# A
name: Person to Avoid
description: This guy will kill you today
type: Image
image: { blueId: 123..456 }

# B
name: Family Member
description: Trust this person
type: Image
image: { blueId: 123..456 }

Different BlueIds (identity content differs), but structural/type checks ignore labels (§3).

14.4 Requirement Overlay → later type binding

# Parent
name: A
prop1:
x: 1 # overlay (no type)
schema: { /* optional constraints */ }

# Child
name: B
type: A
prop1:
type: Some # legal only if merged result preserves x = 1 (and schema)

If Some would force x = 2, resolution fails (§6.2).

14.5 Lists: refine + append

# Parent
name: Trip
segments:
itemType: Flight Segment
# (direct list form)
- { type: Flight Segment, carrier: BA }

# Child
name: Trip LHR->SFO
type: Trip
segments:
- { type: Flight Segment, carrier: BA, from: LHR, to: JFK } # refine index 0
- { type: Flight Segment, carrier: BA, from: JFK, to: SFO } # append

Reorder/delete of inherited prefix elements is invalid; append is valid.

14.6 Expansion with limits

Start from:

blueId: 3JTd8s... # Alice

Expanding /spent/* hydrates only spent:

name: Alice
type: { blueId: GRwTYs... }
age: 25
spent:
amount: 27.15
currency: USD

BlueId remains unchanged (§9).

14.7 Minimization

From a resolved snapshot with fully materialized type subtrees, minimize():

  • collapses type objects to { blueId: … } when available,
  • removes structure derivable from the type chain,
  • may collapse large subtrees to { blueId: … } when exact matches are known.

The minimized authoring form re-resolves to the same snapshot and BlueId.

14.8 Circular-set BlueId

# Authoring (placeholders after algorithm determines positions)
# Dog (#0)
name: Dog
owner: { type: { blueId: 'this#1' } }

# Person (#1)
name: Person
pet: { type: { blueId: 'this#0' } }

The ordered list [Dog, Person] yields MASTER. Final identities: Dog = MASTER#0, Person = MASTER#1.


Part II — Contracts & Document Processing (normative)

Purpose. Part II defines how a Blue document changes state when an event arrives. The processor is a deterministic function:

PROCESS(document, event) → (new_doc, triggered_events, total_gas)

  • new_doc — the updated document after all work (including cascades, bridging, and FIFO drains).
  • triggered_events — the root-scope events emitted during the run, including lifecycle items (e.g., Document Processing Initiated, any terminal Document Processing Fatal Error), whether or not they were handled.
  • total_gas — a deterministic tally of gas units consumed during the run (handlers, cascades, and drains).

Events are Blue nodes. Channels interpret Blue nodes by type/shape (and, where relevant, local routing context).


15. Contracts: how documents become executable

Every object node MAY contain a contracts dictionary:

contracts: { <key: Text> → <value: Contract> }   # see Appendix A (“Contract”)

Contracts tell the processor what to do with a node. Each contract is identified by a type BlueId (published elsewhere in the registry). A processor either supports a contract’s type BlueId or returns a must-understand failure (see §22.1).

Contract roles (types in Appendix A):

  • Channels — where events may arrive. A channel advertises which events it accepts and may adapt/reshape external payloads into channelized form for handlers (see Channel, plus concrete channels in Appendix A §A.4).
  • Handlers — logic bound to exactly one channel (same scope). A handler may (i) request document changes (list of Json Patch Entry, Appendix A), (ii) emit new events (Blue nodes), (iii) consumeGas(units: Integer), and (iv) terminate(cause, reason?). No other side effects are permitted. (See Handler in Appendix A.)
  • Markers — informative state & policy. Markers never run logic; they carry rules or state the processor obeys (e.g., Process Embedded, Processing Initialized Marker, Processing Terminated Marker, Channel Event Checkpoint; all in Appendix A).

Matching rule (normative).
Handlers MUST only match channelized events produced by channels in the same scope (the node whose contracts map contains them). There is no engine-level “channel flag”; channels define their own recognition (by payload type/shape and, when needed, scope context).

Ordering (normative).
When multiple contracts match, the processor sorts channels, and then handlers within each channel, by:

  1. order (number; missing = 0), then
  2. key (the contract map key, lexicographic).
    This yields a deterministic run order.

Reserved processor keys under a contracts map (normative).

  • embeddedProcess Embedded (Appendix A)
  • initializedProcessing Initialized Marker (Appendix A)
  • terminatedProcessing Terminated Marker (Appendix A)
  • checkpointChannel Event Checkpoint (Appendix A)

These keys are reserved for the processor’s own use. If any exist with an incompatible type, processing MUST terminate as runtime fatal (§22.2).

Write-protection of reserved keys (normative).
Contracts (handlers or channels) MUST NOT patch any reserved key path (/…/contracts/(embedded|initialized|terminated|checkpoint) or descendants). Attempting to add/replace/remove a reserved key path from a handler/channel is a deterministic runtime fatal at that scope (§22.2). Processor writes to these keys are permitted only as specified in §§20, 22, 23 and via Direct Writes (§19.1).

Read-only contract inputs (normative).
Contracts (channels and handlers) MUST treat delivered event objects as read-only. A contract MUST NOT mutate payload objects it receives; all document changes MUST occur only via explicit patch operations (Appendix A: Json Patch Entry).
Implementation note (non-normative): Processors MAY enforce this by cloning or freezing payloads, or by relying on contract implementations to obey the rule; this specification does not require cloning.

Channel vs Handler capability surface (normative).

  • Channels may: decide event acceptance; read/update the scope’s Channel Event Checkpoint (Appendix A) for their channel key; consumeGas(units); terminate(cause, reason?).
  • Handlers may: apply JSON patches; emit events; consumeGas(units); terminate(cause, reason?).

No other effects are permitted.

(Validated by: T2, T3, T15, T23–T25; reserved-key tamper tests SHOULD be added.)


16. Embedded sub-documents & isolation

Some subtrees are independent documents processed alongside the parent.

16.1 Process Embedded (marker)

A Process Embedded marker (Appendix A) under contracts/embedded declares embedded children:

contracts:
embedded:
# (type defined in Appendix A)
paths:
- /payment
- /shipping
# Absolute JSON Pointers within the current scope

Normative behavior.

  • Dynamic list. The processor reads paths before processing each child and re-reads it after each child finishes; additions/removals/reorderings take effect immediately for the next child.
  • Single presence. There MUST be at most one Process Embedded per contracts map. Multiple → runtime fatal (§22.2).
  • Single document model. All scopes share the same in-memory document. An embedded child patches its subtree in place; parents and ancestors observe changes via Document Update cascades (§17, §21).

16.2 Isolation & boundary rule

Let the current scope be absolute pointer S (e.g., /, /payment, /a/b). Let E be the set of embedded child roots declared by S’s Process Embedded.

A patch applied while executing in scope S is permitted iff:

  • patch.path starts with S, and
  • patch.path is not strictly inside any other embedded domain X ∈ E where X ≠ S.

“Strictly inside” means below the child boundary:

IS_STRICTLY_INSIDE(path, X) = path starts with (X + "/")

Implications (normative).

  • A parent may add/replace/remove an embedded child root as a whole (e.g., add /payment, remove /payment), but MUST NOT reach inside it (e.g., replace /payment/amount).
  • Children may freely patch inside their own subtree (paths of the form S + "/...").
  • Self-root mutation is forbidden. While executing in scope S, a handler or channel MUST NOT target exactly Swith add, replace, or remove. Only ancestors may add/replace/remove a child root. Violations → runtime fatal (§22.2).
  • Root patch target is forbidden. No contract at any scope may target "/" (§21.2).

Rationale. “Cutting/replacing the balloon” is a parent-only operation; scopes only mutate strict descendants of themselves.

(Validated by: T2, T23–T25.)


17. Processor-managed channels (indispensable)

The processor MUST support (and is the only entity that feeds) these channel families (definitions in Appendix A):

  • Document Update Channel.
    Trigger. When a patch is applied, the processor emits one Document Update per scope participating in the cascade: origin → each ancestor → root.
    Match. Subtree semantics: match if the absolute changed path equals or is a descendant of ABS(scope, path) (see §21.3).
    Payload. Scope-relative Document Update (Appendix A): op (add|replace|remove), path (relative), and before/after snapshots. Payload content is uniform within a scope; channels do not reshape processor-managed payloads.
  • Triggered Event Channel.
    Delivers events enqueued by handlers into the scope’s FIFO.
    Drain timing (normative): exactly once per scope at Phase 5 (§19). Never drains during cascades.
  • Lifecycle Event Channel.
    Delivers lifecycle nodes at a scope (e.g., Document Processing Initiated, Document Processing Terminated). Lifecycle handlers may patch/emit; emissions are enqueued and later drained per §19. Lifecycle nodes themselves are also bridgeable upward (see below).
  • Embedded Node Channel.
    Bridges a child scope’s emissions (Triggered events and lifecycle nodes) into the parent after the child finishes, if the parent configured an Embedded Node Channel for that child’s path.

Helper notation used in this Part (§21.3):
ABS(S, P) — absolute pointer for a channel path P declared at scope S.
relativize_pointer(S, ABS) — path relative to S (returns "/" when ABS == S).
relativize_snapshot(S, node) — subtree content as observed at S (clone or read-only view).

(Validated by: T4–T7, T14–T16, T28.)


18. External contracts (extensibility)

Beyond the processor-managed channels, authors may define custom channels, handlers, and markers.

Normative requirements.

  • Must-understand. If a document contains a contract type the processor does not support, the processor MUST NOT run; it returns a must-understand failure (§22.1).
  • External channels must clearly define what they accept (by type/shape) and how they match.
  • Determinism. Handlers must be deterministic. Their only effects are: changeDocument (list of Json Patch Entry), emitEvent (Blue node), consumeGas(units), and terminate(cause, reason?). No other side effects are permitted.
  • Markers may set policies; where the spec mandates, the processor MUST obey them.

(Validated by: T8–T10, T20–T22.)


19. The PROCESS Algorithm (applied at root and at each embedded scope)

Signature. PROCESS(doc, event) → (new_doc, triggered_events, total_gas)
Events are Blue nodes. There is no envelope.

Important timings (normative):

  • Cascades queue, never drain. Document Update cascades execute handlers immediately at each scope; any Triggered emissions are enqueued to that scope’s persistent FIFO and not delivered during the cascade.
  • Bridge then drain. A parent bridges child emissions (if configured) before it drains its own FIFO.
  • One drain per scope. A scope’s FIFO is drained once at the end of that scope’s _PROCESS (Phase 5).

Top-level wrapper

function PROCESS(doc, event):
RUN.root_events = []
RUN.total_gas = 0
RUN.emitted_by_scope = {} # scope → [bridgeable node] (Triggered + lifecycle)
RUN.fifo_by_scope = {} # scope → FIFO (persistent for the run)
RUN.terminated_scopes= {} # scope → true when terminated
try:
(_doc, _scope_emitted) = _PROCESS(doc, event, scope="/")
return (doc, RUN.root_events, RUN.total_gas)
except TERMINAL_FAILURE:
# A fatal termination at root ends the run.
return (doc, RUN.root_events, RUN.total_gas)

Core routine (applied at every scope)

function _PROCESS(doc, event, scope):
scope_bucket = ensure_bucket(RUN.emitted_by_scope, scope) # bridgeable emissions (Triggered + lifecycle)
scope_fifo = ensure_fifo(RUN.fifo_by_scope, scope) # persistent FIFO for THIS scope

# PHASE 1 — Process embedded children (recursive, dynamic)
processed_paths = insertion_ordered_set()
loop:
paths = read_process_embedded_paths(doc, scope) # validates contracts/embedded is Process Embedded (Appendix A)
next_path = first p in paths where p ∉ processed_paths
if next_path is None:
break
if node_exists(doc, next_path):
(doc, _) = _PROCESS(doc, event, scope=next_path) # single document model
processed_paths.add(next_path)
# Re-reads 'paths' after each child: adds/removes/reorders take effect for the next child.
# Stabilization: once a child path enters processed_paths, it will NOT be processed again in this run,
# even if removed then re-added later ("no resurrection" within a run).

# Early-out if this scope was terminated by a child-side cascade or lifecycle
if RUN.terminated_scopes.get(scope, false):
return (doc, RUN.emitted_by_scope[scope])

# PHASE 2 — Initialize this scope (first run only)
if not has_initialization_marker(doc, scope):
pre_init_id = compute_blue_id_at_scope(doc, scope)

# 2.A Lifecycle: Document Processing Initiated (Appendix A)
lifecycle_node = make_initiated_event(pre_init_id) # payload defined in Appendix A
doc = DELIVER_LIFECYCLE(doc, scope, lifecycle_node)

# 2.B Add Processing Initialized Marker under reserved key 'initialized' (Appendix A)
ensure_reserved_empty_or_compatible(doc, scope, "initialized", "Processing Initialized Marker")
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope,
patch = json_patch_add( scope + "/contracts/initialized",
make_initialized_marker(pre_init_id) )) # marker definition in Appendix A

if RUN.terminated_scopes.get(scope, false):
return (doc, RUN.emitted_by_scope[scope])

# PHASE 3 — Match channels; run handlers for the external event
channels = sort_by_order_then_key( find_matching_channels(doc, scope, event) )
for ch in channels:
if RUN.terminated_scopes.get(scope, false):
break

if is_external_channel(ch): # processor-fed families are NOT gated
# Lazy checkpoint creation: if missing, create empty before newness evaluation
if not has_checkpoint_marker(doc, scope):
DIRECT_WRITE(doc, scope + "/contracts/checkpoint", make_empty_checkpoint())
ckpt = read_checkpoint(doc, scope) # must exist now
if not checkpoint_is_new(ckpt, ch.key, event):
continue

handlers = sort_by_order_then_key( find_handlers_for_channel(doc, scope, ch) )
for h in handlers:
res = execute_handler(h, context_for(scope, event, scope_fifo))
RUN.total_gas += res.gas_consumed

# 3.A Apply patches; each applied patch triggers a bottom-up Document Update cascade
for p in res.patches:
if boundary_violation(doc, scope, p): # includes "self-root", "root target", reserved-key write
ENTER_TERMINAL_TERMINATION(doc, scope, "Boundary violation at " + p.path)
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope, patch=p)

# 3.B Record & enqueue Triggered emissions
for t in res.triggered_events:
EMIT_TO_SCOPE(scope, t)

# 3.C Optional termination from handler
if res.terminated:
ENTER_GRACEFUL_TERMINATION(doc, scope, res.termination_reason)

if is_external_channel(ch) and not RUN.terminated_scopes.get(scope, false):
# After successful channel processing, update checkpoint for ch.key with the entire event node
ckpt2 = read_checkpoint(doc, scope) # exists
DIRECT_WRITE(doc, scope + "/contracts/checkpoint", checkpoint_update(ckpt2, ch.key, event))

if RUN.terminated_scopes.get(scope, false):
return (doc, RUN.emitted_by_scope[scope])

# PHASE 4 — Parent-only: bridge each child's emissions via Embedded Node
# Bridge FIRST (may enqueue many items into THIS scope’s FIFO).
for child_path in iteration_order(processed_paths):
embedded_ch = find_embedded_node_channel(doc, scope, child_path)
if not embedded_ch:
continue
child_events = RUN.emitted_by_scope.get(child_path, [])
if child_events.is_empty():
continue
emb_handlers = sort_by_order_then_key( find_handlers_for_channel(doc, scope, embedded_ch) )
for ev in child_events:
for h in emb_handlers:
res = execute_handler(h, context_for(scope, ev, scope_fifo))
RUN.total_gas += res.gas_consumed
for p in res.patches:
if boundary_violation(doc, scope, p):
ENTER_TERMINAL_TERMINATION(doc, scope, "Boundary violation at " + p.path)
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope, patch=p)
for t in res.triggered_events:
EMIT_TO_SCOPE(scope, t)

if RUN.terminated_scopes.get(scope, false):
return (doc, RUN.emitted_by_scope[scope])

# PHASE 5 — Drain THIS scope’s Triggered FIFO (if channel exists)
if has_triggered_event_channel(doc, scope):
doc = DRAIN_TRIGGERED_QUEUE(doc, scope) # drains once

return (doc, RUN.emitted_by_scope[scope])

Notes.

  • Balloon cut-off. If a parent removes a child mid-run, that child’s current handler finishes; no further work (no extra handlers, no FIFO drain) occurs for that scope in this run. Already recorded emissions are preserved and will be bridged to the parent (including Document Processing Terminated on fatal). Re-adding the same path later in the run does not schedule it again (“no resurrection” within a run).
  • Direct Write is defined in §19.1.

Emit & record helpers

# Records a Triggered node as emitted by 'scope' and enqueues it for local delivery.
# - Appends to RUN.emitted_by_scope[scope] (for parent bridging)
# - Enqueues to RUN.fifo_by_scope[scope]
# - If scope is root, also appends to RUN.root_events (returned even if handled)
function EMIT_TO_SCOPE(scope, node):
bucket = ensure_bucket(RUN.emitted_by_scope, scope)
fifo = ensure_fifo(RUN.fifo_by_scope, scope)
bucket.append(node)
fifo.enqueue(node)
if scope == "/":
RUN.root_events.append(node)

# Records a lifecycle node for bridging ONLY (no local Triggered delivery).
function RECORD_BRIDGEABLE(scope, node):
bucket = ensure_bucket(RUN.emitted_by_scope, scope)
bucket.append(node)
if scope == "/":
RUN.root_events.append(node)

Apply patch + immediate bottom-up cascades

function APPLY_PATCH_WITH_CASCADE(doc, origin_scope, patch):
before = snapshot_at(doc, patch.path) # before mutation
doc = apply_json_patch(doc, patch) # §21.2 (absolute pointers, array bounds, auto-materialization)
after = snapshot_at(doc, patch.path) # after mutation (null for remove)

for scope in ancestors_including_self_up_to_root(origin_scope): # e.g., /a/b → [/a/b, /a, /]
channels = sort_by_order_then_key(
filter_subtree_matches_by_absolute_path(
find_document_update_channels(doc, scope),
changed_abs_path = patch.path))

if channels.is_empty():
continue

# One immutable payload object per scope (same for all handlers in S)
event = make_document_update_event(scope, patch.op, patch.path, before, after) # Appendix A “Document Update”

fifo = ensure_fifo(RUN.fifo_by_scope, scope)
for ch in channels:
handlers = sort_by_order_then_key( find_handlers_for_channel(doc, scope, ch) )
for h in handlers:
res = execute_handler(h, context_for(scope, event, fifo))
RUN.total_gas += res.gas_consumed
for nested in res.patches:
if boundary_violation(doc, scope, nested): # includes reserved-key write-protection
ENTER_TERMINAL_TERMINATION(doc, scope, "Boundary violation at " + nested.path)
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope, patch=nested)
for emitted in res.triggered_events:
EMIT_TO_SCOPE(scope, emitted)

return doc

Drain a scope’s Triggered FIFO

function DRAIN_TRIGGERED_QUEUE(doc, scope):
trig_ch = get_triggered_event_channel(doc, scope)
if not trig_ch:
return doc

fifo = ensure_fifo(RUN.fifo_by_scope, scope)
while not fifo.is_empty():
ev = fifo.dequeue()
handlers = sort_by_order_then_key( find_handlers_for_channel(doc, scope, trig_ch) )
for h in handlers:
res = execute_handler(h, context_for(scope, ev, fifo))
RUN.total_gas += res.gas_consumed
for p in res.patches:
if boundary_violation(doc, scope, p):
ENTER_TERMINAL_TERMINATION(doc, scope, "Boundary violation at " + p.path)
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope, patch=p)
for t in res.triggered_events:
EMIT_TO_SCOPE(scope, t)
return doc

Deliver Lifecycle at a scope

function DELIVER_LIFECYCLE(doc, scope, lifecycle_node):
# Record lifecycle node for bridging (always)
RECORD_BRIDGEABLE(scope, lifecycle_node)

life_channels = sort_by_order_then_key( find_lifecycle_channels(doc, scope) )
fifo = ensure_fifo(RUN.fifo_by_scope, scope)
for ch in life_channels:
handlers = sort_by_order_then_key( find_handlers_for_channel(doc, scope, ch) )
for h in handlers:
res = execute_handler(h, context_for(scope, lifecycle_node, fifo))
RUN.total_gas += res.gas_consumed
for p in res.patches:
if boundary_violation(doc, scope, p):
ENTER_TERMINAL_TERMINATION(doc, scope, "Boundary violation at " + p.path)
doc = APPLY_PATCH_WITH_CASCADE(doc, origin_scope=scope, patch=p)
for t in res.triggered_events:
EMIT_TO_SCOPE(scope, t)
return doc


19.1 Direct Writes (normative)

A Direct Write is a processor mutation that does not produce a Document Update cascade and does not schedule any cascade work. It is used only for:

  • Writing the Processing Terminated Marker (Appendix A) at a scope on termination (§22).
  • Creating a Channel Event Checkpoint (Appendix A) at a scope lazily when an external channel is first evaluated and no checkpoint exists (§23).
  • Updating the checkpoint’s lastEvents[channelKey] after successful external channel processing (§23).

Direct Writes are visible to subsequent logic in the same run and persist in new_doc. They appear only under reserved keys (e.g., /…/contracts/terminated, /…/contracts/checkpoint). Handlers/channels cannot perform Direct Writes; they are processor-internal (§15, §23).


20. Initialization (first-run)

On the first processing of a scope:

  1. Compute the pre-init BlueId of the scope’s subtree.
  2. Publish Document Processing Initiated (Appendix A) via the scope’s Lifecycle Event Channel (root also records it in triggered_events).
  3. Add Processing Initialized Marker (Appendix A) under contracts/initialized (this patch MUST cause a Document Update cascade).

Reserved keys are listed in §15; incompatible presence is fatal (§22.2).

No eager checkpoint creation. The processor MUST NOT create contracts/checkpoint during initialization solely to satisfy presence. The checkpoint is created lazily per §23 when an external channel is first evaluated at this scope.

(Validated by: T3, T15.)


21. Document Updates & JSON Patch semantics

21.1 Document Update cascades (immediate & bottom-up)

One patch → a cascade of deliveries. When a handler’s patch applies successfully, the processor:

  • captures before/after snapshots at the patch’s absolute path,
  • delivers exactly one Document Update (Appendix A) per scope along the chain: origin → each ancestor → root, in that order.

At each scope S:

  • Matching uses the patch’s absolute changed path vs ABS(S, P) with subtree semantics (equal or descendant).
  • Delivered event has uniform content within S. All handlers at S see the same op and the same before/aftersnapshots; the path is scope-relative to S. Contracts MUST treat this event as read-only (§15).
  • Handlers may patch/emit/consumeGas/terminate.
  • No drain during cascades (normative). Triggered emissions produced during a cascade are enqueued to S’s persistent FIFO and MUST NOT be delivered during the cascade. S’s FIFO is drained once, at the end of S’s _PROCESS (Phase 5 in §19).

(Validated by: T4, T5, T14, T17.)

21.2 JSON Patch semantics (normative)

Handlers emit Json Patch Entry objects (Appendix A). The processor applies each patch immediately to the shared document, then performs the Document Update cascade (§21.1). Blue supports a practical, deterministic subset of RFC 6902:

  • Supported operations: add, replace (upsert semantics), remove.
    Other ops (move, copy, test, …) → deterministic runtime fatal.

Pointer evaluation.

  • Patch path values are absolute JSON Pointers (must begin with /); segments are interpreted literally.
  • Numeric segments are allowed (arrays).
  • - is accepted (array append).
  • Escaped tokens (e.g., ~1) are treated verbatim (per JSON Pointer).
  • Pointers resolve against the current document state, after earlier patches in the same run.

Root-document target forbidden (normative).
The target path MUST NOT be "/" (document root). Replacing/removing the entire document is forbidden.

Object auto-materialization.
Missing intermediate objects are materialized as empty objects before applying the patch. Auto-created containers are part of the same patch; the Document Update describes the final requested path.

Object targets.
add inserts a new member or replaces an existing member.
replace behaves as upsert.
remove deletes a member. Removing a non-existent member → deterministic runtime fatal.

Array targets.
Segments may be numeric indices or -.
add with index inserts at that position (shifting).
add with - appends.
replace overwrites element at index (index must exist).
remove deletes the element at index (shifts left; index must exist).
Out-of-range indices (for replace/remove) → deterministic runtime fatal.

Boundary enforcement.
Every patch is validated against embedded-scope isolation (§16). Any attempt to modify another scope’s interior → deterministic runtime fatal. A parent may add/replace/remove a child root (e.g., /payment), but MUST NOT reach inside it (e.g., /payment/amount). A child MAY NOT target its own scope root S (self-root mutation forbidden).

These rules let authors grow deep structures and manipulate arrays predictably while preserving the one-patch → one-cascade guarantee and determinism.

21.3 Helper notation (normative)

ABS(S, P) — the absolute JSON Pointer for a channel path P declared at scope S (normalized concatenation of S and P).
relativize_pointer(S, ABS) — the relative pointer from scope S’s root to ABS (returns "/" when ABS == S).
relativize_snapshot(S, node) — a read-only view or clone of node as observed at S. Nodes carry no path context; the relative path provides the location for handlers.


22. Failure & termination semantics

22.1 Capability failure (must-understand)

If the document contains a contract type the processor does not support, the processor MUST return a must-understand failure and MUST NOT mutate the document or emit lifecycle fatals. (T9.)

22.2 Termination (graceful or fatal)

A channel or handler may invoke terminate(cause, reason?), and the processor may terminate a scope fatally for deterministic runtime errors (e.g., boundary violation).

When a scope terminates (either cause):

  1. Direct Write contracts/terminated with a Processing Terminated Marker (Appendix A), setting:
  • cause: "graceful" or "fatal"
  • reason: <optional Text>
  1. Publish Document Processing Terminated at that scope (Appendix A; Lifecycle Event Channel).
  • Lifecycle is also recorded for bridging via RECORD_BRIDGEABLE (§19).
  1. Deactivate the scope for the rest of the run: mark RUN.terminated_scopes[scope]=true and drop its FIFO; further patch/emit from that scope are no-ops.

Escalation rules.

  • Non-root fatal → scope-terminal only: preserve already recorded emissions for parent bridging; continue elsewhere.
  • Root fatal → run-terminal: additionally append Document Processing Fatal Error (Appendix A) to the root run result (outbox-only; never routed) and abort the run (raise TERMINAL_FAILURE).
  • Non-root graceful → scope ends normally; parent may still bridge the scope’s emissions (including the Terminatedlifecycle) if configured.

(Validated by: T23–T30.)

Termination helpers (informative pseudocode).

function ENTER_GRACEFUL_TERMINATION(doc, scope, reason?):
DIRECT_WRITE(doc, scope + "/contracts/terminated",
make_terminated_marker(cause="graceful", reason=reason))
term_ev = make_terminated_event(cause="graceful", reason=reason) # Appendix A
doc = DELIVER_LIFECYCLE(doc, scope, term_ev)
RUN.terminated_scopes[scope] = true
clear_fifo(RUN.fifo_by_scope, scope)
if scope == "/":
# Root graceful ends the run after current call stack unwinds
raise TERMINAL_FAILURE_GRACEFUL
return doc

function ENTER_TERMINAL_TERMINATION(doc, scope, reason):
DIRECT_WRITE(doc, scope + "/contracts/terminated",
make_terminated_marker(cause="fatal", reason=reason))
term_ev = make_terminated_event(cause="fatal", reason=reason) # Appendix A
doc = DELIVER_LIFECYCLE(doc, scope, term_ev)
RUN.terminated_scopes[scope] = true
clear_fifo(RUN.fifo_by_scope, scope)
if scope == "/":
# Outbox-only fatal lifecycle
RUN.root_events.append(make_fatal_outbox(reason)) # Appendix A
raise TERMINAL_FAILURE
return doc


23. Channel Event Checkpoints

Purpose. A Channel Event Checkpoint records, per external channel key, the last processed event node (entire node, not just an id), enabling idempotent and ordered processing for external sources. (Type in Appendix A.)

Presence & lazy creation.
A scope may lack contracts/checkpoint until it first evaluates an external channel. When an external channel at scope S is evaluated and contracts/checkpoint is absent, the processor MUST DIRECT_WRITE(S + "/contracts/checkpoint", make_empty_checkpoint()) before applying the newness test. From that point on, the scope has exactly one checkpoint. Multiple checkpoints in a scope are a deterministic runtime fatal.

Gating rule (external channels only).
For a matched external channel at scope S:

  • The processor consults the scope’s checkpoint to decide whether the incoming event is newer than lastEvents[channelKey] (deterministic policy).
  • If not newer, the channel is skipped (no handlers run).
  • If newer and the channel completes successfully, the processor MUST DIRECT_WRITE an update setting lastEvents[channelKey] to the entire event node (no Document Update is emitted).

Not gated. Processor-managed families (Document Update, Triggered, Lifecycle, Embedded Node) are never subject to checkpoint gating.

Tamper resistance. Reserved keys are write-protected (§15). Any handler attempt to patch contracts/checkpoint (or descendants) is a deterministic runtime fatal.

(Validated by: T8, T18 (lazy creation), T19–T22.)


24. Gas accounting

24.1 Philosophy & unit (normative)

Purpose. Gas accounting prevents resource-exhaustion while remaining proportional to actual work.

Unit. Gas is an abstract deterministic unit. Processors MUST NOT base gas on wall-clock, CPU model, memory, or I/O. (Informative calibration: profiles MAY document a human-readable mapping; conformance depends solely on the formulas below.)

24.2 What is charged and when (normative)

Processors MUST add the following charges to RUN.total_gas at the exact points indicated. Charges apply in addition to any explicit consumeGas(units) from channels/handlers.

24.2.1 Scope management

OperationFormulaCharge point
Scope entry50 + 10 × depthOn entry to _PROCESS for a scope (root depth=0)
Scope exit0On return from _PROCESS
Initialization (first run)1000When §20 initialization starts for a scope (covers initiated lifecycle + marker patch orchestration)

depth = number of embedded edges from root (/→0; /a/b→2).

24.2.2 Matching & routing

OperationFormulaCharge point
Channel match attempt5 per channel testedEach channel considered in §19 Phase 3
Handler call overhead50Just before executing each handler

24.2.3 Patches & cascades

OperationFormulaCharge point
Boundary check2 per patchBefore applying each patch
Patch: add/replace20 + ceil(bytes/100)After validation, before cascade
Patch: remove10After validation, before cascade
Cascade routing10 per participating scopeFor each scope that receives the resulting Document Update

bytes = UTF-8 length of the canonical JSON of the patch val after Part I §8.2 cleaning/normalization (RFC 8785).

24.2.4 Event emission, bridging & draining

OperationFormulaCharge point
Emit event20 + ceil(bytes/100)When emitEvent(node) succeeds (enqueued)
Bridge child → parent10 per bridged nodeFor each node delivered by Embedded Node Channel in §19 Phase 4 (Triggered or lifecycle)
Drain FIFO10 per dequeued eventFor each event dequeued in §19 Phase 5 (before handler overhead)

bytes = canonical JSON size of the emitted event node.

24.2.5 Checkpoints & direct writes

OperationFormulaCharge point
Checkpoint read0When a channel consults the scope checkpoint
Checkpoint update (Direct Write)20After a matching external channel completes successfully
Termination marker write (Direct Write)20When writing contracts/terminated (§22)

Direct Write is defined in §19.1; it never triggers Document Update.

24.2.6 Lifecycle & termination

OperationFormulaCharge point
Lifecycle delivery30Per DELIVER_LIFECYCLE call (once per call, before any handlers)
Graceful termination overhead+0On terminate(cause="graceful") (marker + lifecycle already charged)
Fatal termination overhead+100On fatal termination (in addition to termination marker & lifecycle delivery)
Must-understand failure0§22.1 capability failure (pre-execution)

Fatal termination total for the termination step: 150 gas (= marker 20 + lifecycle 30 + fatal overhead 100), plus any prior work already charged. Graceful termination step: 50 gas (= marker 20 + lifecycle 30).

24.3 Accumulation & determinism (normative)

RUN.total_gas MUST include:

  • all processor charges from §24.2, and
  • all explicit consumeGas(units) calls made by channels/handlers.

Gas MUST be computed solely from:

  • operation counts (handlers run, events enqueued/dequeued/bridged, scopes in cascade),
  • structural properties (scope depth, cascade length),
  • measurable sizes (canonical JSON byte length).

Gas MUST NOT depend on:

  • wall-clock time, CPU speed/architecture, memory pressure,
  • network/IO latency, OS scheduler effects.

Given the same input document and event, all conforming processors MUST return the same total_gas.

24.4 Policies & overruns (normative)

Accounting only. Part II defines gas accounting, not enforcement. Gas is recorded to RUN.total_gas for transparency and diagnostics. Absent an active policy, the processor MUST NOT modify behavior or terminate due to gas usage; it simply continues and returns the measured total.

Policy‐driven enforcement (external). Budgets/limits MAY be defined by separate, document-authored policies (markers/contracts outside this Part). Such policies may specify what budgets exist, when/how measurements are compared to budgets, and the exact behavior on overrun (e.g., graceful/fatal termination per §22, skipping work, emitting signals, etc.). The processor MUST implement any present policy it claims to support exactly. If a policy type is present but unsupported, the processor MUST return a must-understand failure (§22.1).

Determinism. Policies MUST be deterministic and base decisions only on data available within the run (including the gas totals defined in §24), to preserve cross-implementation consistency.

24.5 Validation examples (informative)

  • Simple replace at root (~4 bytes) — Matching (1×5) + handler overhead (50) + patch replace (20) + cascade (1×10) → 85 gas (+ any handler consumeGas).
  • Deep scope entry (depth=10) first run, small patch — Entry (50+100) + init (1000) + handler (50) + replace (~100B → 21) + cascade (2×10) → 1231 gas (+ handler consumeGas).
  • 2 KB replace across 5 scopes — Replace (20+20) + cascade (5×10) + handler overhead (5×50) → 340 gas (+ handler logic at each scope).
  • Emit 500×1 KB events — Emissions (500×(20+10)) + drain (500×10) + handler overhead (500×50) → 45 000 gas(+ handler consumeGas).

Sizes use canonical JSON per Part I §8.2.

24.6 Implementation hooks (informative)

Where to add charges in the algorithm (§19):

  • _PROCESS entry: scope-entry charge.
  • §20 init path: init charge; lifecycle delivery charge; if a checkpoint marker is created, do not charge gas for this creation.
  • Phase 3: per channel match attempt; per handler overhead; per patch boundary check + op charge + cascade charge (count scopes actually receiving Document Update); per emitted event charge.
  • Phase 4: per bridged node charge (before running handlers).
  • Phase 5: per dequeued event charge (before handler overhead).
  • §22 termination: termination marker Direct Write, lifecycle delivery, and fatal overhead (if fatal).

Bytes measurement: apply Part I §8.2 cleaning & RFC 8785 canonicalization; use UTF-8 length of the canonical JSON string; use ceil(bytes/100) in formulas.


25. Processor vs Feeder (division of responsibility)

Feeder (out of scope here) — collects external events, orders/deduplicates them per the application’s policy (scheduler, vector clocks, chain logs…). The feeder may deliver stale/out-of-order items; Channel Event Checkpoint ensures scopes ignore stale external events.

Processor (this Part) — given (document, event), executes exactly the rules in this Part (initialization, matching, cascades, bridging, FIFO drains, failures/termination) to produce (new_doc, triggered_events, total_gas).


26. Conformance checklist (normative)

A compliant processor MUST:

Embedded traversal

  • Read Process Embedded paths dynamically, re-reading after each child, and process each existing child at most once per run in first-seen order (no resurrection within a run).
  • Enforce the boundary rule (§16): the parent can add/replace/remove the child root, but MUST NOT patch strictly inside another active embedded subtree.
  • Enforce self-root mutation forbidden and root "/" target forbidden.

Contract capabilities

  • Enforce must-understand: any unsupported contract type → capability failure (no mutation, no lifecycle fatals).
  • Use deterministic sorting for channels and handlers: (order, key) at every scope.
  • Treat delivered payloads as immutable (§15).
  • Enforce write-protection of reserved keys (§15).

Initialization

  • On first run at a scope: publish Document Processing Initiated and add Processing Initialized Marker (patch → Document Update).
  • Do not create a checkpoint at init time; create lazily per §23.

Patch & cascade semantics

  • After every applied patch, emit exactly one Document Update per scope in the cascade: origin → ancestors → root.
  • Match Document Update channels by absolute changed path vs ABS(S,P) with subtree semantics.
  • Deliver a scope-relative payload; all handlers at a scope MUST see the same Document Update payload object for a given patch.
  • Maintain a per-scope Triggered FIFO; never drain during cascades; drain once at the end of the scope’s _PROCESS(Phase 5).
  • Record every emission at its scope; append root-scope emissions and root lifecycle items to the run’s triggered_events.

JSON Patch subset

  • Implement supported ops: add, replace (upsert), remove. Reject other RFC 6902 ops.
  • Pointers are absolute; support numeric indices and - for append.
  • Do not allow the document root "/" as a patch target.
  • Auto-materialize missing intermediate objects.
  • Enforce array bounds and object existence (out-of-range / remove-missing → deterministic runtime fatal).

Checkpoint presence & behavior

  • Exactly one Channel Event Checkpoint per scope after first external evaluation (Appendix A). Duplicate → runtime fatal.
  • For external channels, gate using the checkpoint’s newness rule; skip stale/duplicate events.
  • After successful channel processing, Direct Write lastEvents[channelKey] = <entire event node> (no Document Update).
  • Processor-managed families are not gated.

Failure/termination

  • On termination, Direct Write Processing Terminated Marker with cause (fatal/graceful), publish Document Processing Terminated, and deactivate the scope for the remainder of the run.
  • On root fatal, also append Document Processing Fatal Error to the root run result and abort the run.

Run result

  • Return (new_doc, triggered_events, total_gas) exactly as defined in §15/§19.

27. Test vectors (normative, behavior-defining)

T1 — Dynamic embedded list
Root declares paths: [/a, /b]. While processing /a, a root handler removes /b and adds /c.
Then: After /a, the processor re-reads paths and visits /c; /b is skipped (no longer exists).

T2 — Boundary enforcement
Root attempts replace /a/x while /a is an active embedded child.
Then: Fatal termination at root; contracts/terminated is written with Processing Terminated Marker (cause: fatal), a Document Processing Fatal Error is appended to the root run result; run aborts.

T3 — Initialization once
First run at /a writes Processing Initialized Marker (patch) and publishes Document Processing Initiated.
Then: The patch triggers a Document Update cascade; lifecycle may trigger additional work at /a.

T4 — Update cascades (absolute match & relative payload)
A handler at /a applies replace /a/z/k. Root has a Document Update Channel watching /a/z.
Then: At scope /a, payload path="/z/k"; at root, "/a/z/k". Matching uses absolute paths; each scope sees the same payload content relative to itself.

T5 — Cascade emissions are enqueued (not delivered)
Patch at /a/b causes a Document Update handler at /a to emit E.
Then: E is recorded under /a and enqueued; it is delivered later in /a’s Phase 5 (not during the cascade).

T6 — Triggered FIFO (deterministic order)
A handler at /a emits E1, E2. /a has a Triggered Event Channel.
Then: /a drains FIFO (E1 then E2); further emissions during drain append to the tail and are processed deterministically.

T7 — Bridging child emissions
Child /x emits events during its run. Parent has an Embedded Node Channel for /x.
Then: After parent completes external handling, it bridges /x’s emissions (patches cascade), then drains the parent FIFO once.

T8 — Checkpoint gating (external only)
A scope checkpoint exists. Two external channels match the event; one is stale, one is new.
Then: The stale one is skipped; the new one runs; checkpoint updates via Direct Write.

T9 — Capability failure (must-understand)
Document contains unknown contract type.
Then: Processor returns must-understand failure; no patches; no lifecycle fatals.

T10 — No-match
No channel matches anywhere.
Then: Processor returns unchanged document, empty triggered_events, measured total_gas.

T11 — Object auto-materialization
A handler applies add /a/b/c {…} where /a exists but /a/b does not.
Then: Processor creates /a/b as {} then writes /a/b/c; one cascade runs.

T12 — Array append and insert
Given /a/items: ["x","y"].
add /a/items/- "z"["x","y","z"] (append).
add /a/items/1 "q"["x","q","y"] (insert).
Then: Each patch triggers one cascade; out-of-range indices → fatal (T13).

T13 — Deterministic runtime fatals (arrays & objects)
replace /a/items/7 "z" when length < 8 → fatal.
remove /a/missingKey → fatal.
Then: The failing scope gets contracts/terminated with Processing Terminated Marker (cause: fatal); root fatal only if the scope is root.

T14 — Scope-relative payload
Patch at /a/b to replace /a/b/x.
Then: At /a/b: path="/x", at /a: "/b/x", at root: "/a/b/x". Same op, before, after; only path is scope-relative.

T15 — Root lifecycle inclusion
First processing at root publishes Document Processing Initiated; later, a fatal elsewhere.
Then: Root run result includes both lifecycle items in triggered_events.

T16 — Local delivery depends on Triggered Channel presence
During a cascade at /a, a handler emits E. /a lacks a Triggered Event Channel.
Then: E is recorded as emitted by /a (bridgeable upward if parent configured) but is not delivered locally at /a.

T17 — Uniform event per scope
Multiple Document Update Channels at /a match the same patch.
Then: All handlers at /a see the same Document Update payload object; differences arise only from channel/handler order/matching.

T18 — Lazy checkpoint creation
A scope processes an external event and contracts/checkpoint is absent.
Then: Before newness evaluation, the processor DIRECT_WRITEs an empty checkpoint at /…/contracts/checkpoint(lastEvents = {}). Newness evaluates as “no prior”; if handlers succeed, lastEvents[channelKey] becomes the entire event node by Direct Write. No Document Update is emitted for either write.

T19 — Duplicate checkpoint
A scope contains two Channel Event Checkpoint markers.
Then: Runtime fatal (only one is permitted).

T20 — Stale external event is gated
lastEvents.testEventsChannel holds E_old; incoming E_new is not newer.
Then: Channel is skipped; checkpoint unchanged.

T21 — Checkpoint updated after success
A new external event on testEventsChannel is processed successfully.
Then: lastEvents.testEventsChannel = <entire event node> via Direct Write (no Document Update).

T22 — Multiple external channels at a scope
Two external channels match the same event and both are “newer.”
Then: Both run in (order, key) order; each updates its own key in lastEvents.

T23 — Self-root mutation is forbidden
While executing at /a, a contract attempts remove /a (or replace /a, add /a).
Then: Fatal termination at /a.

T24 — Root-document mutation is forbidden
Any contract attempts to target "/" with any op.
Then: Fatal termination at the executing scope (root fatal ends the run).

T25 — Balloon cut-off on child removal
While /b is being processed, a parent watcher removes /b.
Then: The current /b handler finishes; no further work (no extra handlers, no drain) occurs for /b; already recorded /b emissions (including Terminated on fatal) are bridgeable; re-adding /b in this run does not schedule it again.

T26 — Termination is final
A scope terminates gracefully; later in the same run a handler at that scope attempts to emit or patch.
Then: No-op; scope is inactive for the remainder of the run.

T27 — Child fatal does not escalate by default
/a terminates fatally.
Then: /a is marked terminated; parent continues; child emissions (including Terminated) are bridgeable if parent configured Embedded Node Channel.

T28 — Child graceful termination bridges lifecycle
/a terminates gracefully.
Then: Parent may observe Document Processing Terminated via Embedded Node Channel if configured.

T29 — Root graceful termination ends run
Root terminates gracefully.
Then: Run ends; root outbox includes Document Processing Terminated.

T30 — Root fatal termination ends run with fatal outbox
Root terminates fatally.
Then: Run ends; root outbox includes Document Processing Terminated and Document Processing Fatal Error.


Appendix A — Contract & Processor Type Catalog (in Blue)

A.1 Base Types

name: Contract
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Base for all contracts (channels, handlers, markers). Contracts live under a
scope’s `contracts` map (keyed by Text). At runtime (Part II), contract
processors execute deterministically and only through explicit operations;
there are no implicit side effects.
order:
type: Integer
description: Deterministic sort key within a scope; missing ≡ 0.

name: Json Patch Entry
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Deterministic subset of RFC 6902 used by handlers to request document changes
(Part II §21.2). NOTE: field is named `val` (not `value`) because `value`
has special meaning as Blue’s scalar wrapper in Part I; using `val` prevents
shape collisions with wrapper equivalence.
op:
type: Text
schema:
required: true
enum: [add, replace, remove]
path:
type: Text
description: >
Absolute JSON Pointer within the document (must begin with "/").
Runtime forbids targeting "/" (root) as a patch destination (Part II §21.2).
schema:
required: true
val:
description: >
Payload for `add` and `replace` (any Blue node). Omitted for `remove`.

A.2 Contract Subtypes (abstract)

name: Channel
type: Contract
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Abstract base for event entry points within a scope. Channels decide whether
an incoming event matches at this scope (Part II). External channels may also
use the scope’s checkpoint to gate duplicates/stale events.

name: Handler
type: Contract
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Abstract base for logic bound to exactly one channel (same scope). At runtime
(Part II), a handler may: (1) apply patches (list of Json Patch Entry),
(2) emit events (Blue nodes), (3) consume gas via `consumeGas(units: Integer)`,
and (4) terminate (gracefully or fatally). No other effects are permitted.
channel:
type: Text
description: >
The contracts-map key of the channel this handler binds to (same scope).
schema:
required: true
event:
description: >
Optional matcher payload used by the handler’s processor to further restrict
which channelized events it will handle. IMPORTANT: the matching strategy
(shape checks, field tests, etc.) is defined by the specific handler
processor, not by this base schema.

name: Marker
type: Contract
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Abstract base for informational/policy contracts. Markers do not run logic;
they carry state/policy enforced by the processor (Part II).

A.3 Required Markers

name: Process Embedded
type: Marker
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Declares embedded child scopes beneath the current scope (Part II §16).
The processor reads this list dynamically and re-reads after each child finishes.
paths:
type: List
itemType:
type: Text
description: >
Scope-relative absolute pointers to child roots (strings beginning with "/",
resolved against the current scope).
schema:
required: true
uniqueItems: true

name: Processing Initialized Marker
type: Marker
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Recorded exactly once at a scope on first processing; stores the pre-init
BlueId of the scope’s subtree (Part II §20). Writing this marker is a patch
that triggers a Document Update cascade.
documentId:
type: BlueId
schema:
required: true

name: Processing Terminated Marker
type: Marker
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Final state for a scope (either graceful or fatal). Once present, the scope
becomes permanently inactive both for the remainder of the current run and in
subsequent runs until explicitly replaced by a parent. Written as a Direct
Write (no Document Update) when termination occurs (Part II §22).
cause:
type: Text
schema:
required: true
enum: [fatal, graceful]
reason:
type: Text
description: Optional human-readable explanation.

name: Channel Event Checkpoint
type: Marker
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Stores last-seen events per external channel at this scope to enable
idempotent processing and ordering (Part II §23). Updates are Direct Writes
(no Document Update).
lastEvents:
name: Last Events
description: >
Map of channelKey (the contracts key of an external channel in this scope)
to the entire last event node seen for that channel. Values are unconstrained
(any Blue node) to allow channel-specific shapes.
type: Map
keyType:
type: Text
# valueType intentionally omitted → any Blue node
schema:
required: true

A.4 Processor-Fed Channels

name: Document Update Channel
type: Channel
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Fires on successful patches with immediate bottom-up cascade
(origin → ancestors → root). Matching uses subtree semantics against
ABS(scope, path). Payload is the processor-emitted 'Document Update' event
with scope-relative path (Part II §17, §21.1).
path:
type: Text
description: >
Scope-relative absolute pointer (begins with "/") defining the watched
subtree. Match iff the absolute changed path equals or is a descendant
of ABS(scope, path).
schema:
required: true

name: Triggered Event Channel
type: Channel
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Delivers events previously enqueued by handlers into the scope’s FIFO.
One drain per scope at the end of scope processing; never drains during
cascades (Part II §17, §19).

name: Lifecycle Event Channel
type: Channel
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Delivers processor lifecycle notifications at this scope, e.g.,
'Document Processing Initiated' and 'Document Processing Terminated'
(Part II §17, §20, §22).

name: Embedded Node Channel
type: Channel
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Bridges a child scope’s emissions (including lifecycle nodes) into the parent
after the child finishes (Part II §17, §19).
childPath:
type: Text
description: >
Scope-relative absolute pointer to the child root to bridge.
schema:
required: true

A.5 Processor-Emitted Events

name: Document Update
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Emitted once per participating scope for each successful patch
(bottom-up delivery). 'op' uses lower-case enum; 'path' is scope-relative
for the receiving scope. 'before' and 'after' are snapshots (immutable views)
(Part II §21.1).
op:
type: Text
schema:
required: true
enum: [add, replace, remove]
path:
type: Text
description: >
Scope-relative pointer. "/" when the receiving scope’s root was affected.
schema:
required: true
before:
description: Snapshot before the patch at this path (may be null).
after:
description: Snapshot after the patch at this path (may be null; often null for remove).

name: Document Processing Initiated
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Published once at a scope on first processing (before writing the
Processing Initialized Marker). At root, it is also included in the run’s
'triggered_events' outbox (Part II §20).
documentId:
type: BlueId
schema:
required: true

name: Document Processing Terminated
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Published at the terminating scope when processing ends, either gracefully
or fatally. Bridgeable to the parent via Embedded Node Channel if configured
(Part II §22).
cause:
type: Text
schema:
required: true
enum: [fatal, graceful]
reason:
type: Text
description: Optional explanation.

name: Document Processing Fatal Error
description: >
Core type of Blue Language v1.0 (see https://language.blue).
Appended to the root run result (outbox only; never routed via channels)
when any scope terminates fatally. Conveys where and why the fatal occurred
(Part II §22).
domain:
type: Text
description: Absolute scope pointer where the fatal occurred (e.g., "/a/b").
schema:
required: true
code:
type: Text
schema:
required: true
enum: ["RuntimeFatal"]
reason:
type: Text
description: Optional explanation.