Skip to main content

Decorations

Status: Out of v4.0.0 (decision 0014); returns as its own design with a specified merge algebra the kit can pin.

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