Skip to main content

Decorations

Decorations provide a way to attach additional metadata to IR nodes without modifying the core IR structure. They are stored separately from the IR in a layered, composable system.

Design Principles

  • Separation of Concerns: Core IR remains clean; decorations are additive metadata
  • Layered: Multiple decoration sources with defined precedence
  • Flattened: FQName keys for direct lookup, not mirrored directory structure
  • Type-Safe: Decoration values are validated against Morphir-defined schemas
  • Deep Merge: Same node/decoration from multiple layers are deep-merged

VFS Structure

.morphir-dist/
├── format.json
├── pkg/ # Core IR
├── deps/ # Dependencies
└── deco/ # Decorations (flattened, layered)
├── format.json # Decoration system metadata

├── schemas/ # Local schema cache
│ ├── documentation.ir.json
│ ├── deprecated.ir.json
│ └── tags.ir.json

└── layers/ # Decoration layers
├── core/ # Built-in (from source)
│ ├── manifest.json
│ ├── documentation.json
│ └── deprecated.json

├── tooling/ # Generated by tools
│ ├── manifest.json
│ ├── type-info.json
│ └── complexity.json

└── user/ # User-defined
├── manifest.json
├── documentation.json
├── deprecated.json
└── tags.json

File Formats

Decoration Format File (deco/format.json)

Top-level metadata for the decoration system.

{
"formatVersion": "4.0.0",
"schemaRegistry": {
"documentation": {
"displayName": "Documentation",
"description": "Structured documentation for IR nodes",
"localPath": "schemas/documentation.ir.json",
"remoteRef": "morphir://registry/decorations/documentation@1.0.0",
"entryPoint": "morphir/decorations:documentation#documentation",
"cachedAt": "2026-01-16T12:00:00Z"
},
"deprecated": {
"displayName": "Deprecated",
"description": "Mark nodes as deprecated with migration info",
"localPath": "schemas/deprecated.ir.json",
"remoteRef": "morphir://registry/decorations/deprecated@1.0.0",
"entryPoint": "morphir/decorations:deprecated#deprecation"
},
"tags": {
"displayName": "Tags",
"description": "Arbitrary string tags for categorization",
"localPath": "schemas/tags.ir.json",
"entryPoint": "morphir/decorations:tags#tag-set"
}
},
"layers": ["core", "tooling", "user"],
"layerPriority": {
"core": 0,
"tooling": 50,
"user": 100
}
}

Layer Manifest (layers/{layer}/manifest.json)

Metadata for a specific decoration layer.

{
"formatVersion": "4.0.0",
"layer": "user",
"displayName": "User Annotations",
"description": "User-defined decorations and overrides",
"priority": 100,
"decorationTypes": ["documentation", "deprecated", "tags"],
"createdAt": "2026-01-16T12:00:00Z",
"updatedAt": "2026-01-16T14:30:00Z"
}

Decoration Values (layers/{layer}/{decoration-type}.json)

Flattened decoration values keyed by FQName.

{
"formatVersion": "4.0.0",
"decorationType": "documentation",
"layer": "user",
"values": {
"my-org/project:domain#user": {
"summary": "Represents a user in the system",
"details": [
"Contains identity and contact information.",
"Immutable after creation."
],
"examples": [
{
"title": "Basic user",
"code": "{ email = \"alice@example.com\", name = \"Alice\" }"
}
]
},
"my-org/project:domain#create-user": {
"summary": "Creates a new user account",
"params": {
"email": "A valid email address",
"name": "The user's display name"
},
"returns": "The created User record",
"throws": ["InvalidEmail", "DuplicateUser"]
},
"my-org/project:domain": {
"summary": "Core domain types and operations",
"moduleDoc": true
}
}
}

Deprecated Decoration Example

{
"formatVersion": "4.0.0",
"decorationType": "deprecated",
"layer": "user",
"values": {
"my-org/project:legacy#old-calculate": {
"since": "2.0.0",
"replacement": "my-org/project:domain#calculate",
"reason": "Performance improvements and bug fixes",
"removalVersion": "3.0.0",
"migrationGuide": "https://docs.example.com/migrate-calculate"
},
"my-org/project:legacy#legacy-user": {
"since": "1.5.0",
"replacement": "my-org/project:domain#user",
"reason": "Renamed for consistency"
}
}
}

Tags Decoration Example

{
"formatVersion": "4.0.0",
"decorationType": "tags",
"layer": "user",
"values": {
"my-org/project:domain#user": ["pii", "core-domain", "audited"],
"my-org/project:domain#calculate-price": ["business-rule", "pricing"],
"my-org/project:reporting#generate-report": ["slow", "async", "batch"]
}
}

Gleam Type Definitions

// === decorations.gleam ===

/// Schema reference - local cache with optional remote source
pub type SchemaRef {
SchemaRef(
display_name: String,
description: Option(String),
local_path: String, // Path within deco/schemas/
remote_ref: Option(String), // Registry reference
entry_point: FQName, // Type defining decoration shape
cached_at: Option(String), // ISO 8601 timestamp
)
}

/// Decoration format metadata
pub type DecorationFormat {
DecorationFormat(
format_version: String,
schema_registry: Dict(String, SchemaRef),
layers: List(String),
layer_priority: Dict(String, Int),
)
}

/// Layer manifest
pub type LayerManifest {
LayerManifest(
format_version: String,
layer: String,
display_name: String,
description: Option(String),
priority: Int,
decoration_types: List(String),
created_at: String,
updated_at: String,
)
}

/// Decoration values file
pub type DecorationValuesFile {
DecorationValuesFile(
format_version: String,
decoration_type: String,
layer: String,
values: Dict(FQName, Dynamic), // Values validated against schema
)
}

/// Node path for decoration targeting
pub type DecorationTarget {
/// Type definition: "my-org/project:domain#user"
TypeTarget(fqname: FQName)
/// Value definition: "my-org/project:domain#create-user"
ValueTarget(fqname: FQName)
/// Module: "my-org/project:domain"
ModuleTarget(package: PackagePath, module: ModulePath)
/// Package: "my-org/project"
PackageTarget(package: PackagePath)
}

Merge Semantics

When the same node has the same decoration type across multiple layers, values are deep-merged with higher priority layers winning on conflicts.

Merge Rules

Value TypeMerge Behavior
Primitive (string, number, bool)Higher priority wins
ObjectDeep merge recursively
ArrayConcatenate (higher priority first), dedupe optionally
NullHigher priority wins (can "unset" lower layer)

Merge Example

Layer: core (priority 0)
{
"my-org/project:domain#user": {
"summary": "A user",
"details": ["From source"]
}
}

Layer: user (priority 100)
{
"my-org/project:domain#user": {
"summary": "Represents a user in the system",
"examples": [{ "title": "Basic", "code": "..." }]
}
}

Merged Result:
{
"my-org/project:domain#user": {
"summary": "Represents a user in the system", // user wins (higher priority)
"details": ["From source"], // from core (not in user)
"examples": [{ "title": "Basic", "code": "..." }] // from user (not in core)
}
}

Merge Algorithm

/// Deep merge decoration values with priority
pub fn merge_decoration_values(
layers: List(#(Int, Dict(FQName, Dynamic))), // (priority, values)
) -> Dict(FQName, Dynamic) {
layers
|> list.sort(fn(a, b) { int.compare(a.0, b.0) }) // Sort by priority (ascending)
|> list.fold(dict.new(), fn(acc, layer) {
let #(_, values) = layer
dict.fold(values, acc, fn(acc, key, value) {
case dict.get(acc, key) {
Ok(existing) -> dict.insert(acc, key, deep_merge(existing, value))
Error(_) -> dict.insert(acc, key, value)
}
})
})
}

/// Deep merge two dynamic values
fn deep_merge(base: Dynamic, override: Dynamic) -> Dynamic {
// If both are objects, merge recursively
// If both are arrays, concatenate
// Otherwise, override wins
...
}

Layer Semantics

Standard Layers

LayerPrioritySourceDescription
core0CompilationExtracted from source (doc comments, annotations)
tooling50Analysis toolsGenerated by linters, analyzers, type inference
user100ManualUser-authored decorations

Custom Layers

Projects can define additional layers with custom priorities:

{
"layers": ["core", "team-standards", "tooling", "user", "overrides"],
"layerPriority": {
"core": 0,
"team-standards": 25,
"tooling": 50,
"user": 100,
"overrides": 200
}
}

Schema Management

Local Cache

Schemas are cached locally in deco/schemas/ for offline access and performance:

deco/schemas/
├── documentation.ir.json # Full Morphir IR for documentation type
├── deprecated.ir.json # Full Morphir IR for deprecated type
└── tags.ir.json # Full Morphir IR for tags type

Remote Registry Reference

Schemas can reference a remote registry for updates:

{
"localPath": "schemas/documentation.ir.json",
"remoteRef": "morphir://registry/decorations/documentation@1.0.0"
}

Schema Sync

# Sync schemas from registry
morphir decoration schema sync

# Update specific schema
morphir decoration schema update documentation

# Validate local schemas against registry
morphir decoration schema verify

JSON-RPC Methods

deco/read

Read merged decorations for a node.

{
"method": "deco/read",
"params": {
"target": "my-org/project:domain#user",
"decorationType": "documentation" // optional, null = all types
}
}

Response:

{
"result": {
"target": "my-org/project:domain#user",
"decorations": {
"documentation": {
"summary": "Represents a user in the system",
"details": ["..."]
},
"tags": ["pii", "core-domain"]
},
"sources": {
"documentation": ["core", "user"],
"tags": ["user"]
}
}
}

deco/write

Write a decoration value to a specific layer.

{
"method": "deco/write",
"params": {
"target": "my-org/project:domain#user",
"decorationType": "documentation",
"layer": "user",
"value": {
"summary": "Updated documentation"
}
}
}

deco/list

List all decorated nodes.

{
"method": "deco/list",
"params": {
"decorationType": "deprecated", // optional filter
"layer": "user" // optional filter
}
}

deco/layers

List available layers with metadata.

{
"method": "deco/layers",
"params": {}
}

Common Decoration Types

Documentation

type alias Documentation =
{ summary : String
, details : List String
, examples : List Example
, params : Dict String String
, returns : Maybe String
, throws : List String
, see : List String
, since : Maybe String
}

type alias Example =
{ title : String
, code : String
, language : Maybe String
}

Deprecated

type alias Deprecation =
{ since : String
, replacement : Maybe FQName
, reason : Maybe String
, removalVersion : Maybe String
, migrationGuide : Maybe String
}

Tags

type alias TagSet = List String

Alias (Code Generation)

type alias Alias =
{ name : String
, target : Maybe String -- "typescript", "scala", etc.
}

Native Hint

type alias NativeHint =
{ platform : String
, implementation : String
, importPath : Maybe String
}

Validation

Decoration values are validated against their schemas:

# Validate all decorations
morphir decoration validate

# Validate specific layer
morphir decoration validate --layer user

# Validate specific type
morphir decoration validate --type documentation

Validation checks:

  1. Value conforms to decoration schema
  2. Target FQName exists in IR
  3. No orphaned decorations (target was deleted)
  4. Schema versions are compatible

Migration from Legacy Format

The legacy format stored all decorations in a single file:

// Old: my-decoration-values.json
{
"My.Package:Foo:bar": { ... }
}

Migration to VFS format:

morphir decoration migrate --from my-decoration-values.json --layer user --type documentation

This creates:

deco/layers/user/documentation.json