{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://crayon-c4.com/schema/v1/crayon-c4.schema.json",
  "title": "CRAYON-C4 Model File",
  "description": "Portable model file produced by the CRAYON-C4 generator and consumed by the web import-with-merge flow. version 1.",
  "version": 1,
  "type": "object",
  "required": [
    "schemaVersion"
  ],
  "properties": {
    "schemaVersion": {
      "const": 1,
      "description": "Schema contract version. Must equal the version of this schema."
    },
    "meta": {
      "$ref": "#/$defs/ProjectMeta"
    },
    "c4": {
      "$ref": "#/$defs/C4Data"
    },
    "deployCanvas": {
      "$ref": "#/$defs/DeployCanvas"
    },
    "f4": {
      "$ref": "#/$defs/F4Data"
    },
    "f4Layout": {
      "type": "object",
      "description": "Per-LEVEL arrangement + labels for collapsed F4 views (issue #379) — the F4 sibling of c4Layout / deployLayout, keyed \"<diagramId>:<level>\" where level is one of system | container | component. \"code\" is never a key: the fully expanded board IS the model, so its geometry lives on the activities. Presence is what makes a value manual — a card absent from positions is placed by the projection, a card absent from cardLabels says whatever the projection calls it (\"Merged (3)\"); back-to-automatic deletes the entry. Every key must reference an existing diagram (DC-F4-038; orphans are pruned on load, never rejected).",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "positions": {
            "type": "object",
            "description": "Consolidated-card positions at this level, keyed by card id (an activity id, or \"run:<firstStepId>\" for a merged run) and measured lane-locally on the card's host lane.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y"
              ]
            }
          },
          "cardLabels": {
            "type": "object",
            "description": "What a card is called at this level, keyed exactly like positions. Never blank — no override is stored as absence.",
            "additionalProperties": {
              "type": "string",
              "minLength": 1
            }
          },
          "edgeLabels": {
            "type": "object",
            "description": "What a drawn line is called at this level, keyed \"<fromCardId>><toCardId>\" — the pair the consolidated line joins, since several connections may roll up onto it.",
            "additionalProperties": {
              "type": "string",
              "minLength": 1
            }
          },
          "edgeReturnLabels": {
            "type": "object",
            "description": "What a CALL's answer arrow says at this level, keyed exactly like edgeLabels. The return leg is drawn at every level a call survives, so it gets the same choice its request label has. Absent = the connection's own returnLabel.",
            "additionalProperties": {
              "type": "string",
              "minLength": 1
            }
          },
          "edgeLabelPositions": {
            "type": "object",
            "description": "How far along a drawn line its label sits at this level (0–100), keyed exactly like edgeLabels. Per level because the LINE is: a collapsed view draws it between cards the fold made, so the spot that keeps the text clear of them is not the spot that does so at code. Absent = wherever the connection's own labelPosition puts it.",
            "additionalProperties": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            }
          },
          "edgeRoutes": {
            "type": "object",
            "description": "Author-placed waypoints the drawn line runs through at this level, keyed exactly like edgeLabels. Per level for two reasons, and the second forces it: a collapsed view places the cards elsewhere, so a corridor drawn at code means nothing here; and consolidation is many-to-one, so a level may draw ONE line where several connections roll up, which has no single connection to hang a route on. code is never a key — there the route lives on the connection (F4Connection.points). Absent = the canvas routes the line; back to automatic deletes the entry rather than storing an empty list.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  }
                },
                "required": [
                  "x",
                  "y"
                ]
              }
            }
          }
        }
      }
    },
    "adr": {
      "$ref": "#/$defs/AdrData"
    },
    "c4Layout": {
      "type": "object",
      "description": "Per-diagram view layout (user-owned), keyed by layer id: node position pins, manual boundary-group frames, hand-routed connection lines, chosen connection sides, and where each line's label sits. A group with a frame is manually sized; without one it auto-fits its members.",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "positions": {
            "type": "object",
            "description": "Node position pins, keyed by node id.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y"
              ]
            }
          },
          "groupRects": {
            "type": "object",
            "description": "Manual boundary-group frames, keyed by group id. Presence = manual size mode; absent = auto-fit.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                },
                "width": {
                  "type": "number"
                },
                "height": {
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y",
                "width",
                "height"
              ]
            }
          },
          "edgeSides": {
            "type": "object",
            "description": "Which side of its card each end of a line attaches to on this diagram, keyed like edgeRoutes. An absent end (or entry) is automatic — the diagram picks the side from where the two elements sit.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "from": {
                  "enum": [
                    "t",
                    "r",
                    "b",
                    "l"
                  ]
                },
                "to": {
                  "enum": [
                    "t",
                    "r",
                    "b",
                    "l"
                  ]
                }
              }
            }
          },
          "edgeShapes": {
            "type": "object",
            "description": "How each line is DRAWN on this diagram, keyed like edgeRoutes — a per-line override of the diagram's own edgeStyle. An absent entry means the diagram decides.",
            "additionalProperties": {
              "enum": [
                "curved",
                "straight"
              ]
            }
          },
          "edgeRoutes": {
            "type": "object",
            "description": "Hand-routed connection lines on this diagram, keyed \"<fromNodeId>><toNodeId>\" over the endpoints as this diagram draws them. Presence = manual: the stored points are the COMPLETE corner list of the drawn orthogonal polyline (spec 104) — only the two short legs into the cards are derived — and no automatic router applies; absent = automatic. Points are in this diagram's canvas coordinates. A manual line's two attach sides are stamped in edgeSides when it goes manual and are never re-derived; going back to automatic removes both the route and those sides.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  }
                },
                "required": [
                  "x",
                  "y"
                ]
              }
            }
          },
          "edgeLabelPositions": {
            "type": "object",
            "description": "Where each line's label sits on this diagram: a percentage (0–100) from the line's source end to its target end, keyed \"<fromNodeId>><toNodeId>\" exactly like edgeRoutes. An absent entry means the midpoint, which is why 50 is never stored. Per diagram because a label has to sit where it is readable, and that depends on where this diagram put the cards around it. Older files carry the value on the relationship instead (c4.relationships[].labelPosition); it is folded onto the diagrams that draw the line when the file is loaded.",
            "additionalProperties": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            }
          }
        }
      }
    },
    "deployLayout": {
      "type": "object",
      "description": "Per-LEVEL manual layout for collapsed deployment views (spec 103) — the sibling of c4Layout, keyed \"<environmentId>:<ladderStopToken>\" (never by layer id; the two maps must not be merged). Each bucket has the same shape and manual/auto contract as a c4Layout bucket: presence of a node in positions = manual at that level, absence = auto; back-to-automatic deletes the entry. The floor level never has an entry — floor geometry lives on the placements themselves. Every key must reference an existing environment (DC-DEP-030; orphans are pruned on load, never rejected).",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "positions": {
            "type": "object",
            "description": "Projected-node position pins at this level, keyed by the node's rf id (placementId, or placementId::slot::… for a nested child).",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y"
              ]
            }
          },
          "groupRects": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                },
                "width": {
                  "type": "number"
                },
                "height": {
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y",
                "width",
                "height"
              ]
            }
          },
          "edgeSides": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "from": {
                  "enum": [
                    "t",
                    "r",
                    "b",
                    "l"
                  ]
                },
                "to": {
                  "enum": [
                    "t",
                    "r",
                    "b",
                    "l"
                  ]
                }
              }
            }
          },
          "edgeRoutes": {
            "type": "object",
            "description": "Hand-routed merged lines at this level, keyed \"<fromDefinitionId>><toDefinitionId>\" (the drawn coarse pair).",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  }
                },
                "required": [
                  "x",
                  "y"
                ]
              }
            }
          },
          "edgeLabelPositions": {
            "type": "object",
            "additionalProperties": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            }
          }
        }
      }
    },
    "ui": {
      "type": "object"
    },
    "history": {
      "type": "array"
    }
  },
  "anyOf": [
    {
      "required": [
        "c4"
      ]
    },
    {
      "required": [
        "deployCanvas"
      ]
    },
    {
      "required": [
        "f4"
      ]
    }
  ],
  "$defs": {
    "ProjectMeta": {
      "type": "object",
      "required": [
        "name",
        "filename",
        "createdAt",
        "modifiedAt"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "filename": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "modifiedAt": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "C4ElementType": {
      "enum": [
        "Person",
        "External System",
        "Software System",
        "Container",
        "Component",
        "Code"
      ]
    },
    "C4Data": {
      "type": "object",
      "required": [
        "meta",
        "nodeTypes",
        "nodes",
        "layers"
      ],
      "properties": {
        "meta": {
          "type": "object"
        },
        "version": {
          "type": "integer",
          "description": "spec 095: C4 data-format version. 2 = single central relationships list; absent/1 = legacy per-layer relationships, migrated on load."
        },
        "nodeTypes": {
          "type": "object",
          "description": "Visual styles for the six fixed C4 element types, keyed by element-type name.",
          "additionalProperties": {
            "$ref": "#/$defs/C4NodeTypeStyle"
          }
        },
        "displayTypes": {
          "type": "object",
          "description": "Visual styles for user-defined display types, keyed by display-type name.",
          "additionalProperties": {
            "$ref": "#/$defs/C4NodeTypeStyle"
          }
        },
        "lineTypes": {
          "type": "object",
          "description": "Visual styles for user-defined CONNECTION line types, keyed by line-type name. Project-wide: C4 relationships, deployment connections and F4 connections all name one through their own displayType.",
          "additionalProperties": {
            "$ref": "#/$defs/ConnectionLineStyle"
          }
        },
        "defaultLineStyle": {
          "$ref": "#/$defs/ConnectionLineStyle",
          "description": "What a connection that names NO line type draws as, project-wide and shared by all three models. Filled per field; absent = the built-in look. A look the canvas itself owns (an instance line, an inferred ghost) still wins."
        },
        "groupFillOpacity": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Fill opacity for every boundary group, 0..1. The single dial \u2014 there are no per-group overrides. Absent = the built-in 0.18.",
          "default": 0.18
        },
        "defaultGroupStyle": {
          "type": "object",
          "description": "What a boundary group that names NO display type draws as \u2014 frame, fill and label colour. Project-wide and shared by the C4 and F4 canvases, filled per field; absent = the built-in neutral frame. A group naming a display type takes the type's colours instead.",
          "properties": {
            "color": {
              "type": "string"
            },
            "bg": {
              "type": "string"
            },
            "border": {
              "type": "string"
            }
          }
        },
        "noteStyle": {
          "type": "object",
          "description": "How this project draws its diagram notes (C4Layer.notes) — text/fill/border colours, filled per field over the built-in sticky look. Absent = the built-in look.",
          "properties": {
            "color": {
              "type": "string"
            },
            "bg": {
              "type": "string"
            },
            "border": {
              "type": "string"
            }
          }
        },
        "tagStyles": {
          "type": "object",
          "description": "What each TAG in this project draws as, keyed by the tag itself. Project-wide and shared by all three models for the same reason lineTypes is: `#pci` must be the same colour on a C4 diagram, a deployment environment and an F4 board, or the dots stop being a vocabulary and become decoration. Absent — or a tag absent from it — means the colour DERIVED from the tag's own text.",
          "propertyNames": {
            "$ref": "#/$defs/Tag"
          },
          "additionalProperties": {
            "$ref": "#/$defs/TagStyle"
          }
        },
        "nodes": {
          "type": "object",
          "description": "Flat dictionary of C4 nodes keyed by id. Code-derived ids match the stable-ref pattern.",
          "propertyNames": {
            "$ref": "#/$defs/ElementId"
          },
          "additionalProperties": {
            "$ref": "#/$defs/C4Node"
          }
        },
        "layers": {
          "type": "object",
          "description": "Dictionary of C4 diagrams (layers) keyed by id.",
          "additionalProperties": {
            "$ref": "#/$defs/C4Layer"
          }
        },
        "relationships": {
          "type": "array",
          "description": "spec 095: the single central list of relationships (endpoints are actual node ids). Each diagram derives its edges by projecting these. Per-layer C4Layer.relationships is legacy and folded into this on load.",
          "items": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "type": "string",
                "description": "Source node id (an actual node)."
              },
              "to": {
                "type": "string",
                "description": "Target node id (an actual node)."
              },
              "label": {
                "type": "string"
              },
              "tech": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "bidirectional": {
                "type": "boolean"
              },
              "keepExplicit": {
                "type": "boolean"
              },
              "labelPosition": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "LEGACY. Percent along the edge (0–100) where the label sits. Superseded by the per-diagram c4Layout.edgeLabelPositions: one relationship is drawn on every diagram where both of its ends resolve, and each lays its elements out differently, so a single value cannot be right for all of them. Still accepted on import — it is folded onto the diagrams that draw the line and then dropped. Do not write it in new files."
              },
              "displayType": {
                "type": "string",
                "description": "Name of a connection line style in C4Data.lineTypes (colour / width / dash). A name the project does not define falls back to the default line — never an error."
              }
            }
          }
        },
        "rollupLabels": {
          "type": "object",
          "description": "The label each ROLLED-UP pair shows: what the one line a coarser diagram draws says where several deeper connections meet on it, instead of the [*] fallback. Keyed \"<fromNodeId>><toNodeId>\" over real node ids. Owned by the pair — not by a diagram and not by any contributor — so one label reaches every connection tied to that level and every diagram that draws it. Never holds an entry for a pair that is itself a relationship: that pair is labelled by the relationship's own label.",
          "additionalProperties": {
            "type": "string"
          }
        }
      }
    },
    "C4Layer": {
      "type": "object",
      "description": "One C4 diagram (a layer/view at a given level).",
      "properties": {
        "nodes": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "relationships": {
          "type": "array",
          "description": "Legacy/deprecated (spec 095): relationships now live in the top-level C4Data.relationships central list; this is folded in on load."
        },
        "groups": {
          "type": "array",
          "description": "Boundary groups drawn on this diagram. Only `tags` is declared on the item shape; the rest is open. A DERIVED subject boundary has no stored group at all — it answers to its subject node's tags, exactly as it takes that node's prose.",
          "items": {
            "type": "object",
            "properties": {
              "tags": {
                "$ref": "#/$defs/TagList"
              }
            }
          }
        },
        "label": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "parent": {
          "type": [
            "string",
            "null"
          ]
        },
        "pill": {
          "type": "string"
        },
        "diagramType": {
          "enum": [
            "context",
            "container",
            "component",
            "code",
            "dynamic"
          ]
        },
        "subject": {
          "type": [
            "string",
            "null"
          ],
          "description": "The single element this diagram is about (its center stage), drawn as the boundary group, not as a node. null = explicitly no subject; absent = inferable from the boundary group."
        },
        "edgeStyle": {
          "enum": [
            "curved",
            "straight"
          ],
          "description": "How this diagram draws its connection lines: curved (smooth) or straight (right-angled corners). Absent = curved. Lines with hand-placed waypoints (see c4Layout.edgeRoutes) always follow those corners."
        },
        "notes": {
          "type": "array",
          "description": "Diagram-only annotation shapes (notes) on THIS diagram. Absent = none.",
          "items": {
            "$ref": "#/$defs/DiagramNote"
          }
        }
      }
    },
    "C4Node": {
      "type": "object",
      "required": [
        "name"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "shortDescription": {
          "type": "string"
        },
        "displayType": {
          "type": "string"
        },
        "icon": {
          "type": "string"
        },
        "parent": {
          "type": "string"
        },
        "tags": {
          "type": "object",
          "properties": {
            "type": {
              "$ref": "#/$defs/C4ElementType"
            },
            "tech": {
              "type": "string"
            }
          }
        },
        "tagList": {
          "$ref": "#/$defs/TagList",
          "description": "The author's TAGS on this element. Called `tagList` and NOT `tags` because that name was taken long before tags existed by the bag of structural facts above — `tags.type` is the element type and a hard rule (DC-C4-002). A deployment or F4 element bound to this one INHERITS these and cannot be stripped of them there."
        }
      }
    },
    "C4NodeTypeStyle": {
      "type": "object",
      "description": "Visual style for a C4 element type or display type.",
      "properties": {
        "color": {
          "type": "string",
          "description": "Font / text colour (hex)."
        },
        "bg": {
          "type": "string",
          "description": "Card background colour (hex)."
        },
        "border": {
          "type": "string",
          "description": "Card border colour (hex)."
        },
        "sel": {
          "type": "string",
          "description": "Selection highlight colour (hex)."
        },
        "dash": {
          "type": "boolean",
          "description": "Render the border dashed."
        },
        "icon": {
          "type": "string",
          "description": "Default Material Symbols icon name for nodes of this type (spec 097 US9). Optional — nodes without an explicit icon fall back to this."
        },
        "description": {
          "type": "string",
          "description": "What this style MEANS — shown in the export legend beside its swatch."
        }
      }
    },
    "ConnectionLineStyle": {
      "type": "object",
      "description": "Visual style for a connection line. Every field is optional — an unset one falls back to the canvas's own default line.",
      "properties": {
        "color": {
          "type": "string",
          "description": "Stroke colour (hex)."
        },
        "width": {
          "type": "number",
          "minimum": 0.5,
          "maximum": 12,
          "description": "Stroke width in canvas px."
        },
        "dash": {
          "enum": [
            "solid",
            "dashed",
            "dotted"
          ],
          "description": "How the line is drawn along its length. Absent = solid."
        },
        "arrow": {
          "enum": [
            "none",
            "arrow",
            "arrowclosed",
            "dot",
            "diamond"
          ],
          "description": "The mark where the line meets what it points at. Absent = arrowclosed (a filled arrow), the look every canvas has always drawn."
        },
        "description": {
          "type": "string",
          "description": "What this line style MEANS — shown in the export legend beside its swatch."
        }
      }
    },
    "ElementId": {
      "description": "Code-derived element id. Stable refs look like '<kind>:<8 hex>'; hand-authored editor ids are free-form.",
      "type": "string",
      "anyOf": [
        {
          "pattern": "^(system|container|component|deployNode|flow|activity):[0-9a-f]{8}$"
        },
        {
          "pattern": ".*"
        }
      ]
    },
    "DeployBaseType": {
      "description": "The fixed, closed set of deployment base types (DC-DEP-003).",
      "enum": [
        "Deployment Node",
        "Infrastructure Node",
        "Container Instance",
        "Software System Instance"
      ]
    },
    "DeployTypeStyle": {
      "type": "object",
      "description": "Visual style for a deployment base type or user display type.",
      "properties": {
        "borderColor": {
          "type": "string"
        },
        "fontColor": {
          "type": "string"
        },
        "backgroundColor": {
          "type": "string"
        },
        "icon": {
          "type": "string"
        }
      }
    },
    "DeployInstances": {
      "description": "Replica count: a positive integer, or a range string like \"1..N\" / \"0..5\". Absent = 1.",
      "type": [
        "integer",
        "string"
      ]
    },
    "DeployNodeMeta": {
      "type": "object",
      "description": "What is true of THIS deployment of an element (per-placement). Required on every placement and child placement.",
      "required": [
        "deployDescription"
      ],
      "properties": {
        "deployDescription": {
          "type": "string",
          "description": "Markdown; may be empty."
        },
        "role": {
          "type": "string",
          "description": "This deployment's part — e.g. \"primary\", \"standby\". Draws inline; adds no height."
        }
      }
    },
    "DeployDefinition": {
      "type": "object",
      "description": "A reusable deployable node (deploy-item) or a deployed C4 container (container-drop).",
      "required": [
        "id",
        "kind",
        "baseType",
        "children"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "kind": {
          "enum": [
            "deploy-item",
            "container-drop"
          ],
          "description": "DC-DEP-001."
        },
        "name": {
          "type": "string"
        },
        "tech": {
          "type": "string"
        },
        "icon": {
          "type": "string"
        },
        "baseType": {
          "$ref": "#/$defs/DeployBaseType"
        },
        "type": {
          "description": "Optional user display type. MUST NOT collide with a fixed DeployBaseType (DC-DEP-004).",
          "type": "string",
          "not": {
            "$ref": "#/$defs/DeployBaseType"
          }
        },
        "shortDescription": {
          "type": "string"
        },
        "tags": {
          "$ref": "#/$defs/TagList",
          "description": "The author's TAGS on this deployable element. A container-drop ALSO answers to the tags of the C4 element it deploys — those are read at the point of use and never copied here, so re-tagging the C4 element re-tags every deployment of it."
        },
        "levelTier": {
          "type": "integer",
          "minimum": 1,
          "maximum": 4,
          "description": "Optional abstraction level (spec 103): which of the FOUR levels (1–4) this element collapses at. Level N closes every box marked N or deeper — nothing inside them is shown. The levels are uniform: 1 is the coarsest (mark every root 1 and it reads as the whole environment at a glance) and 4 the finest (mark nothing 4 and it draws everything open, which is the view editing happens in). The NUMBER orders the ladder; every level's display name is per environment (DeployEnvironment.levelNames). Structural and shared across environments. A level on a floor definition — one with nothing inside it — is ignored for laddering (DC-DEP-028 soft warning); the floor is derived from contents, not from baseType."
        },
        "layout": {
          "enum": [
            "vertical",
            "horizontal",
            "grid",
            "manual"
          ]
        },
        "size": {
          "type": "object",
          "properties": {
            "w": {
              "type": "number"
            },
            "h": {
              "type": "number"
            }
          }
        },
        "containerId": {
          "type": "string",
          "description": "For container-drop: MUST equal a C4 Container/Software System node id (DC-DEP-002)."
        },
        "children": {
          "type": "array",
          "description": "Ordered child slots. Their ids must match the childPlacements used when this definition is placed (DC-DEP-012).",
          "items": {
            "type": "object",
            "required": [
              "id",
              "definitionId"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "definitionId": {
                "type": "string"
              }
            }
          }
        }
      },
      "if": {
        "properties": {
          "kind": {
            "const": "container-drop"
          }
        }
      },
      "then": {
        "required": [
          "containerId"
        ]
      }
    },
    "EnvConnectionEndpoint": {
      "type": "object",
      "required": [
        "placementId"
      ],
      "properties": {
        "placementId": {
          "type": "string"
        },
        "childPath": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Slot-id chain root→leaf; absent/empty = root."
        },
        "handle": {
          "enum": [
            "t",
            "r",
            "b",
            "l"
          ],
          "description": "Which side handle the line uses."
        },
        "fanOrder": {
          "type": "number",
          "description": "Where this end sits among the lines sharing its side (lower = first; top of a vertical side, left of a horizontal one). Absent = placed by geometry. Written by the editor's reorder drop."
        }
      }
    },
    "EnvConnection": {
      "type": "object",
      "description": "A connection between two root placements on an environment canvas.",
      "required": [
        "id",
        "from",
        "to",
        "hidden"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "from": {
          "$ref": "#/$defs/EnvConnectionEndpoint"
        },
        "to": {
          "$ref": "#/$defs/EnvConnectionEndpoint"
        },
        "labelOverride": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "technology": {
          "type": "string"
        },
        "hidden": {
          "type": "boolean"
        },
        "labelPosition": {
          "type": "number",
          "minimum": 0,
          "maximum": 100
        },
        "points": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              }
            }
          }
        }
      }
    },
    "DeployChildPlacement": {
      "type": "object",
      "description": "Per-environment facts about a child slot. `id` matches a definition child slot id.",
      "required": [
        "id",
        "meta",
        "childPlacements"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "size": {
          "type": "object",
          "properties": {
            "w": {
              "type": "number"
            },
            "h": {
              "type": "number"
            }
          }
        },
        "instanceGroupId": {
          "type": "string"
        },
        "instances": {
          "$ref": "#/$defs/DeployInstances"
        },
        "meta": {
          "$ref": "#/$defs/DeployNodeMeta"
        },
        "childPlacements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/DeployChildPlacement"
          }
        }
      }
    },
    "DeployPlacement": {
      "type": "object",
      "description": "A top-level placement of a deploy-item definition within an environment.",
      "required": [
        "id",
        "definitionId",
        "position",
        "meta",
        "childPlacements"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "definitionId": {
          "type": "string"
        },
        "position": {
          "type": "object",
          "required": [
            "x",
            "y"
          ],
          "properties": {
            "x": {
              "type": "number"
            },
            "y": {
              "type": "number"
            }
          }
        },
        "size": {
          "type": "object",
          "properties": {
            "w": {
              "type": "number"
            },
            "h": {
              "type": "number"
            }
          }
        },
        "instanceGroupId": {
          "type": "string"
        },
        "instances": {
          "$ref": "#/$defs/DeployInstances"
        },
        "deploymentGroup": {
          "type": "string"
        },
        "meta": {
          "$ref": "#/$defs/DeployNodeMeta"
        },
        "childPlacements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/DeployChildPlacement"
          }
        }
      }
    },
    "DeployEnvironment": {
      "type": "object",
      "description": "One environment canvas. `placements` and `connections` are required arrays (DC-DEP-027) — absent is not the same as empty.",
      "required": [
        "id",
        "name",
        "placements",
        "connections"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "placements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/DeployPlacement"
          }
        },
        "connections": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/EnvConnection"
          }
        },
        "levelNames": {
          "type": "object",
          "description": "Display names for the four abstraction levels ON THIS DIAGRAM (spec 103), keyed \"1\"..\"4\". An absent key means the default — \"Minimal\" for 1, \"Full\" for 4, \"Level N\" otherwise. Names are per environment because what a level MEANS is this diagram's business; the NUMBER is what orders the ladder and what definitions reference, so renaming never re-points anything.",
          "propertyNames": {
            "enum": [
              "1",
              "2",
              "3",
              "4"
            ]
          },
          "additionalProperties": {
            "type": "string",
            "minLength": 1
          }
        },
        "notes": {
          "type": "array",
          "description": "Diagram-only annotation shapes (notes) on this environment's canvas (absolute env-canvas coords). Absent = none.",
          "items": {
            "$ref": "#/$defs/DiagramNote"
          }
        }
      }
    },
    "DeployCanvas": {
      "type": "object",
      "description": "Deployment topology slice: reusable definitions placed into environments. `definitions` and `environments` are REQUIRED arrays (DC-DEP-027); an empty array is fine, an absent one is not.",
      "required": [
        "definitions",
        "environments"
      ],
      "properties": {
        "meta": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string"
            },
            "titleIcon": {
              "type": "string"
            },
            "description": {
              "type": "string"
            },
            "defaultChildLayout": {
              "enum": [
                "vertical",
                "horizontal",
                "grid",
                "manual"
              ]
            }
          }
        },
        "version": {
          "type": "integer"
        },
        "activeEnvironmentId": {
          "type": "string"
        },
        "definitions": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/DeployDefinition"
          }
        },
        "environments": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/DeployEnvironment"
          }
        },
        "instanceGroups": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "label": {
                "type": "string"
              }
            }
          }
        },
        "instanceConnections": {
          "type": "array",
          "description": "One instance's INTERNAL wiring, drawn inside every placement of it. The item shape is open. A connection carries NO tags of its own in any model — it is drawn as the least visible of the two elements it joins — so there is nothing here to declare.",
          "items": {
            "type": "object"
          }
        },
        "baseTypeStyles": {
          "type": "object",
          "description": "Visual styles for the four fixed base types — ALL FOUR keys required (DC-DEP-022).",
          "required": [
            "Deployment Node",
            "Infrastructure Node",
            "Container Instance",
            "Software System Instance"
          ],
          "additionalProperties": {
            "$ref": "#/$defs/DeployTypeStyle"
          }
        },
        "typeStyles": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/$defs/DeployTypeStyle"
          }
        },
        "rollupLabels": {
          "type": "object",
          "description": "Labels for ROLLED-UP connections at collapsed abstraction levels (spec 103), keyed \"<fromDefinitionId>><toDefinitionId>\". Owned by the definition pair — one label reaches every environment that draws the pair. A merged line with no entry renders the multi-connection fallback \"[*]\". Every key must parse to two existing definitions (DC-DEP-029; orphans are pruned on load, never rejected).",
          "additionalProperties": {
            "type": "string"
          }
        }
      }
    },
    "F4DiagramLink": {
      "type": "object",
      "required": [
        "targetDiagramId"
      ],
      "properties": {
        "targetDiagramId": {
          "type": "string"
        },
        "label": {
          "type": "string"
        }
      }
    },
    "F4Section": {
      "type": "object",
      "required": [
        "id",
        "diagramId",
        "order"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "diagramId": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "order": {
          "type": "number"
        },
        "position": {
          "type": "object",
          "properties": {
            "x": {
              "type": "number"
            },
            "y": {
              "type": "number"
            }
          }
        },
        "width": {
          "type": "number"
        }
      }
    },
    "F4Swimlane": {
      "type": "object",
      "required": [
        "id",
        "diagramId",
        "sectionId",
        "name",
        "order"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "diagramId": {
          "type": "string"
        },
        "sectionId": {
          "type": "string"
        },
        "parentLaneId": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "order": {
          "type": "number"
        },
        "c4Ref": {
          "type": "string"
        },
        "deployRef": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "diagramLinks": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4DiagramLink"
          }
        },
        "displayType": {
          "type": "string"
        },
        "height": {
          "type": "number"
        },
        "tags": {
          "$ref": "#/$defs/TagList",
          "description": "The author's TAGS on this lane. OPTIONAL where the four element shapes below require the array: lanes have never carried tags, so an absent list is what every existing board has. A lane bound to a C4 element (`c4Ref`) also answers to that element's tags and cannot be stripped of them here."
        }
      }
    },
    "F4BoundaryEvent": {
      "type": "object",
      "required": [
        "id",
        "activityId",
        "type",
        "tags",
        "exitConnectionId"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "activityId": {
          "type": "string"
        },
        "type": {
          "enum": [
            "T",
            "E",
            "C",
            "S",
            "M"
          ],
          "description": "Timer / Error / Compensation / Signal / Message."
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "tags": {
          "type": "array",
          "description": "The author's TAGS. A REQUIRED array on this shape (it has carried tags since v1), so `[]` is the canonical form for none — unlike everywhere else, where absence is.",
          "items": {
            "$ref": "#/$defs/Tag"
          }
        },
        "exitConnectionId": {
          "type": "string",
          "description": "Id of a connection whose fromType is boundaryEvent."
        },
        "affectedOutputs": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "F4Activity": {
      "type": "object",
      "description": "One behavioral step. `tags`, `dataAnnotations`, `boundaryEvents` are REQUIRED arrays (use []); `position` is required.",
      "required": [
        "id",
        "diagramId",
        "swimlaneId",
        "name",
        "tags",
        "dataAnnotations",
        "boundaryEvents",
        "position"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "diagramId": {
          "type": "string"
        },
        "swimlaneId": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "tags": {
          "type": "array",
          "description": "The author's TAGS. A REQUIRED array on this shape (it has carried tags since v1), so `[]` is the canonical form for none — unlike everywhere else, where absence is.",
          "items": {
            "$ref": "#/$defs/Tag"
          }
        },
        "dataAnnotations": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "boundaryEvents": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4BoundaryEvent"
          }
        },
        "position": {
          "type": "object",
          "required": [
            "x",
            "y"
          ],
          "properties": {
            "x": {
              "type": "number"
            },
            "y": {
              "type": "number"
            }
          }
        },
        "size": {
          "type": "object",
          "properties": {
            "w": {
              "type": "number"
            },
            "h": {
              "type": "number"
            }
          }
        },
        "c4Ref": {
          "type": "string"
        },
        "diagramLinks": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4DiagramLink"
          }
        }
      }
    },
    "F4Connection": {
      "type": "object",
      "description": "Directed arrow. There is no `tags` field — a connection carries none in any model; it is drawn as the least visible of the two activities it joins, and a list stored by an older file is stripped on load. There is no `dashed` field either — a dash is a line style (c4.lineTypes) named via `displayType`, and a boundary-event exit is drawn dashed from its `fromType` alone. kind:\"call\" = synchronous invoke-and-return (the viewer draws the return).",
      "required": [
        "id",
        "diagramId",
        "fromId",
        "fromType",
        "toId"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "diagramId": {
          "type": "string"
        },
        "fromId": {
          "type": "string"
        },
        "fromType": {
          "enum": [
            "activity",
            "boundaryEvent"
          ]
        },
        "toId": {
          "type": "string"
        },
        "label": {
          "type": "string",
          "description": "What the line SAYS on the canvas — short."
        },
        "description": {
          "type": "string",
          "description": "Markdown prose about the connection (what crosses it, and why). Shown in the side panel and carried into the export; distinct from the on-canvas `label`."
        },
        "labelPosition": {
          "type": "number",
          "minimum": 0,
          "maximum": 100
        },
        "kind": {
          "enum": [
            "flow",
            "call"
          ]
        },
        "returnLabel": {
          "type": "string"
        },
        "outOrder": {
          "type": "number"
        },
        "points": {
          "type": "array",
          "description": "Author-placed waypoints this line runs through at code — the fully expanded board. Presence = hand-routed: the line runs through these and no automatic router applies; absent = the canvas routes it. Stored ON the connection, unlike a C4 diagram's edgeRoutes, because an F4 connection belongs to exactly one diagram and so its corridor has only one picture to be true of (the same reason a deployment connection carries its own points). A COLLAPSED level draws this line between cards the fold made, so its route lives per level in f4Layout[\"<diagramId>:<level>\"].edgeRoutes instead.",
          "items": {
            "type": "object",
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              }
            },
            "required": [
              "x",
              "y"
            ]
          }
        },
        "edgeShape": {
          "type": "string",
          "enum": [
            "curved",
            "straight"
          ],
          "description": "How this ONE line is drawn, overriding its diagram's edgeStyle. Absent = the diagram decides, which is what the pane offers as \"By board\"."
        }
      }
    },
    "F4Group": {
      "type": "object",
      "required": [
        "id",
        "diagramId",
        "label",
        "memberLaneIds",
        "memberActivityIds",
        "tags"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "diagramId": {
          "type": "string"
        },
        "label": {
          "type": "string"
        },
        "memberLaneIds": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "memberActivityIds": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "parentGroupId": {
          "type": "string"
        },
        "c4Ref": {
          "type": "string"
        },
        "tags": {
          "type": "array",
          "description": "The author's TAGS. A REQUIRED array on this shape (it has carried tags since v1), so `[]` is the canonical form for none — unlike everywhere else, where absence is.",
          "items": {
            "$ref": "#/$defs/Tag"
          }
        },
        "displayType": {
          "type": "string"
        },
        "opacity": {
          "type": "number"
        }
      }
    },
    "F4Diagram": {
      "type": "object",
      "description": "A flat behavioral board. Holds ≥1 section.",
      "required": [
        "id",
        "processId",
        "name",
        "sections",
        "swimlanes",
        "activities",
        "connections",
        "groups"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "processId": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "sections": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Section"
          }
        },
        "swimlanes": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Swimlane"
          }
        },
        "activities": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Activity"
          }
        },
        "connections": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Connection"
          }
        },
        "groups": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Group"
          }
        },
        "notes": {
          "type": "array",
          "description": "Diagram-only annotation shapes (notes) on this board (absolute canvas coords). Absent = none.",
          "items": {
            "$ref": "#/$defs/DiagramNote"
          }
        },
        "edgeStyle": {
          "type": "string",
          "enum": [
            "curved",
            "straight"
          ],
          "description": "How this board draws every line that does not override it. Absent = \"straight\" (right angles), which is what every F4 board has always drawn — note this is the OPPOSITE default from C4Layer.edgeStyle, deliberately, so each notation keeps the look it shipped with. A single connection deviates with its own edgeShape."
        }
      }
    },
    "F4Process": {
      "type": "object",
      "required": [
        "id",
        "name",
        "diagrams"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "diagrams": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Diagram"
          }
        }
      }
    },
    "F4UIState": {
      "type": "object",
      "description": "F4 view state. The board's OWN tag filter (`activeTags` + `tagMode`, and the hard DC-F4-031 that guarded it) was RETIRED: tags are now a project-wide vocabulary and the filter is the device-local `ui.tagView`, which sits outside the document rev, so selecting a tag can never dirty a project or become a change a teammate has to merge. Both keys are stripped when the project is loaded.",
      "properties": {
        "selectedProcessId": {
          "type": "string"
        },
        "selectedDiagramId": {
          "type": "string"
        }
      }
    },
    "F4Data": {
      "type": "object",
      "description": "Behavioral F4 slice: processes contain flat diagrams. `processes` and `ui` are required.",
      "required": [
        "processes",
        "ui"
      ],
      "properties": {
        "processes": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/F4Process"
          }
        },
        "ui": {
          "$ref": "#/$defs/F4UIState"
        }
      }
    },
    "AdrData": {
      "type": "object",
      "description": "Architecture Decision Records (issue #356). Each ADR links (many-to-many) to C4 nodes, C4 relationships and deployment items; every link carries a stable target identity plus a soft reference (name + type captured at link time). Field-level contract in docs/reference/07-adr-schema.md.",
      "required": [
        "adrs"
      ],
      "properties": {
        "adrs": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Adr"
          }
        }
      }
    },
    "Adr": {
      "type": "object",
      "required": [
        "id",
        "title",
        "status",
        "date",
        "links"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Stable id (adr-NNNN); survives element renames."
        },
        "title": {
          "type": "string"
        },
        "status": {
          "enum": [
            "proposed",
            "accepted",
            "deprecated",
            "superseded"
          ]
        },
        "date": {
          "type": "string",
          "description": "Decision date, YYYY-MM-DD."
        },
        "context": {
          "type": "string",
          "description": "Markdown."
        },
        "decision": {
          "type": "string",
          "description": "Markdown."
        },
        "consequences": {
          "type": "string",
          "description": "Markdown."
        },
        "categories": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "User-created classification (e.g. data, infrastructure) — as many as apply. Emergent vocabulary; names are comma-free."
        },
        "supersedes": {
          "type": "string",
          "description": "Id of the ADR this one supersedes (linear chain)."
        },
        "supersededBy": {
          "type": "string",
          "description": "Id of the ADR that supersedes this one (linear chain)."
        },
        "needsReview": {
          "const": true,
          "description": "Present-true when a linked element was removed; cleared by an explicit acknowledge."
        },
        "lastConfirmed": {
          "type": "string",
          "description": "YYYY-MM-DD of the last still-valid acknowledgement."
        },
        "links": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/AdrLink"
          }
        }
      }
    },
    "AdrLink": {
      "type": "object",
      "required": [
        "target",
        "name",
        "elementType"
      ],
      "properties": {
        "target": {
          "type": "object",
          "required": [
            "kind"
          ],
          "properties": {
            "kind": {
              "enum": [
                "c4-node",
                "c4-relationship",
                "deploy-definition",
                "deploy-placement"
              ]
            },
            "nodeId": {
              "type": "string"
            },
            "from": {
              "type": "string"
            },
            "to": {
              "type": "string"
            },
            "definitionId": {
              "type": "string"
            },
            "environmentId": {
              "type": "string"
            },
            "placementId": {
              "type": "string"
            }
          }
        },
        "name": {
          "type": "string",
          "description": "Soft reference: display name at link time. Always populated."
        },
        "elementType": {
          "type": "string",
          "description": "Soft reference: element type at link time."
        }
      }
    },
    "Tag": {
      "type": "string",
      "pattern": "^#[a-z][a-z0-9-]*$",
      "description": "A tag: `#` followed by lowercase letters, digits and hyphens (`#pci`, `#phase-2`). ONE shape for every tag in every model — a tag says nothing about what an element IS (that is its element type) and nothing about how it is drawn (that is its display type), which is exactly why one vocabulary can be shared by the C4, deployment and F4 models. Anything else is normalised (capitals lowered, a missing `#` added) or dropped when the project is loaded."
    },
    "TagList": {
      "type": "array",
      "description": "The author's TAGS on this element — the project-wide vocabulary the tag bar filters by. Absent = none; an EMPTY array is never written, because a content hash cannot tell `[]` from absence and a field that is sometimes one and sometimes the other manufactures save conflicts. (The four F4 element shapes are the exception: their `tags` has been a required array since v1, so there `[]` is the canonical form.) Colours live in c4.tagStyles.",
      "items": {
        "$ref": "#/$defs/Tag"
      }
    },
    "TagStyle": {
      "type": "object",
      "description": "What one tag draws as. A tag ABSENT from c4.tagStyles takes the colour derived from its own text — stable across projects and machines, and costing nothing to store, which is what keeps an untouched project byte-identical.",
      "required": [
        "color"
      ],
      "properties": {
        "color": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Dot colour, `#rrggbb`."
        }
      }
    },
    "DiagramNote": {
      "type": "object",
      "description": "A diagram-only annotation shape (a note). Shared by all three models: C4Layer.notes, DeployEnvironment.notes and F4 diagram notes carry this shape, styled by the ONE project-wide C4Data.noteStyle. Never an element of any model: not a node, never a connection endpoint, and its position is intrinsic (there is no auto-layout for annotations).",
      "required": [
        "id",
        "text",
        "position"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "text": {
          "type": "string",
          "description": "Short label drawn as the note's title."
        },
        "description": {
          "type": "string",
          "description": "Body prose drawn on the note below the title."
        },
        "position": {
          "type": "object",
          "required": [
            "x",
            "y"
          ],
          "properties": {
            "x": {
              "type": "number"
            },
            "y": {
              "type": "number"
            }
          }
        },
        "width": {
          "type": "number",
          "description": "Width in canvas units; absent = the default note width. Height always grows to fit the text."
        },
        "anchors": {
          "type": "array",
          "description": "Elements ON THIS DIAGRAM the note points at, drawn as dashed leader lines (the UML comment-anchor). Diagram-only like the note itself: an anchor is NEVER a relationship or connection of any model — it says nothing about the architecture, and it is pruned when its target is no longer on the diagram. C4 only today (the canvas that draws them). Absent = none.",
          "items": {
            "type": "string"
          }
        }
      }
    }
  }
}
