Generated banner for Architecture Diagrams
Generated logo for Architecture Diagrams

Architecture Diagrams

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Content generator
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Architecture diagram authoring for cloud infrastructure and declared data catalogs. Use when rendering Azure IaC or DS_CATALOG_V1 relationships as caller-selected ASCII or Mermaid diagrams.
— the description by Microsoft

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Architecture Diagrams skill provides a standardized way to visualize cloud infrastructure and data catalogs. It processes infrastructure configuration files (such as Terraform, Bicep, or Kubernetes manifests) or declared DS_CATALOG_V1 data models to produce consistent, readable diagrams. The skill supports both ASCII block diagrams and Mermaid flowcharts, ensuring that structural boundaries and relationships are preserved across formats. It acts as a faithful view over source authority, requiring explicit scope and adhering to strict parsing rules to ensure accuracy.

What it does

  • Renders infrastructure topologies from files like Terraform, Bicep, ARM, and Kubernetes manifests.
  • Visualizes DS_CATALOG_V1 data models as entity-relationship diagrams.
  • Supports user-selectable output in either ASCII block format or Mermaid code blocks.
  • Maintains consistent boundaries, relationships, and labels across different output formats.
  • Provides a structured output contract including a legend and key relationship details.
  • Persists user format preferences to a local state file for consistent future generation.

When to use it

  • When you need to document cloud infrastructure topology for ADRs or design reviews.
  • When visualizing relationships within a declared DS_CATALOG_V1 data model.
  • When you need a consistent, text-based representation of system architecture that can be version-controlled.
  • When you require a clear, standardized legend and relationship summary for architectural documentation.

Advantages

  • Ensures diagrams remain synchronized with source files by treating them as a view over source authority.
  • Provides flexibility by supporting both human-readable ASCII and machine-renderable Mermaid formats.
  • Enforces strict adherence to declared data models, preventing the invention of unauthorized relationships.
  • Standardizes documentation output with a consistent structure for legends and key relationships.

Usage examples

“Generate an architecture diagram for the infrastructure in the ./infra folder.”
The agent identifies the infrastructure files, asks for a preferred format (ASCII or Mermaid) if not already set, and generates a diagram showing the services, networking, and dependencies.
“Create a data model diagram for the current DS_CATALOG_V1 catalog.”
The agent parses the catalog entities and relationships, rendering an ER diagram in the preferred format with a legend explaining multiplicity and confidence levels.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/microsoft/architecture-diagrams/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip architecture-diagrams.zip -d .agents/skills

Documentation by Microsoft

Architecture Diagrams Skill

Goal

Turn infrastructure source files or a declared DS_CATALOG_V1 data model into a readable architecture diagram for reviews, ADRs, and design discussions. Preserve the caller's selected output format and the source's authority boundaries.

Infrastructure inputs include Terraform, Bicep, ARM templates, shell scripts, Kubernetes manifests, and Docker or Compose files. Catalog input uses declared entities and relationships from data-catalog, the durable data-catalog skill. It does not infer a data model from SQL or ORM files.

This skill documents infrastructure topology and data models. To document a software system, meaning its containers, its components, and the people and systems around it, use the c4-architecture skill instead.

Success criteria

  • The diagram includes only the confirmed source scope.

  • Infrastructure sources retain their existing parsing and relationship behavior.

  • Catalog diagrams preserve declared entity IDs, endpoints, cardinality, endpoint minimums, join keys, confidence, and evidence basis without inventing relationships.

  • Caller preference controls ASCII or Mermaid output.

  • Inferred and assumed catalog relationships remain visibly distinct from confirmed relationships.

Constraints

  • Treat a diagram as a view over source authority, not a semantic authority of its own.

  • Read catalog-erd.md for DS_CATALOG_V1 input, multiplicity mapping, confidence rendering, the catalog output contract, and the Functional Planner compatibility boundary.

  • Do not parse SQL DDL, Prisma, SQLAlchemy, or another ORM as catalog input.

  • Do not render primary-key, foreign-key, or uniqueness markers for catalog join keys; the catalog declares field names, not database key roles.

  • Keep the Feasibility Study Interchange Profile and downstream requirement mappings in their owning workstreams.

Stop rules

  • Stop and ask for scope when infrastructure boundaries are ambiguous.

  • Stop and report an unsupported catalog version, unresolved endpoint, unknown cardinality, missing or invalid endpoint minimum, unknown confidence value, or malformed join-key declaration instead of guessing or partially rendering.

  • Stop before diagram generation when no output preference can be resolved.

Output Format

This skill produces either ASCII block diagrams or Mermaid diagrams. Neither is the default: the caller or surrounding context chooses the output format for each diagram. When the caller does not state a preference, ask which format they want before generating.

The diagram type follows the source type. Infrastructure sources render as ASCII block diagrams or Mermaid flowcharts using the ASCII Conventions or Mermaid Conventions below. Catalog sources render as ASCII entity lines or a Mermaid erDiagram using catalog-erd.md. In every case, keep the structure, boundaries, and relationships identical across formats.

Preference Contract

Architecture diagram format selection applies beyond ADRs. For standalone usage, consult the root state file at .copilot-tracking/architecture-diagrams/state.json. If that file is absent, create it when a format is chosen.

{
  "userPreferences": {
    "diagramFormat": "mermaid"
  },
  "repoVisibility": "private"
}

The userPreferences.diagramFormat value must be either ascii or mermaid. The repoVisibility field is optional and may be used by surrounding workflows when they need to distinguish public and private repositories. Resolution order is:

  1. Explicit request or caller state.

  2. Root state file at .copilot-tracking/architecture-diagrams/state.json.

  3. If no preference is known for a standalone request, ask once and persist the answer to the root state file for later reuse.

Workflow

Follow this sequence when authoring a diagram:

  1. Discovery. Identify the relevant infrastructure files or declared catalog and the architectural scope. When the scope is unclear, ask which folders, services, or entities should be included.

  2. Parsing. For infrastructure, extract services, data stores, networking components, ingress points, and deployment units. For a catalog, execute scripts/render_catalog_erd.py or follow catalog-erd.md without adding inferred semantics.

  3. Relationship mapping. For infrastructure, connect components with the correct direction. For a catalog, retain the exact declared endpoints, cardinality, endpoint minimums, join keys, confidence, and basis.

  4. Generation. Render the final diagram in the caller's chosen format, with clear boundaries or entity labels and a compact legend.

ASCII Conventions

Use consistent box notation and alignment:

+------------------+      +------------------+
|   Service Name   |----->|   Service Name   |
+------------------+      +------------------+

Use the following conventions for readability:

  • Keep box labels short and specific.

  • Keep arrows aligned and use one relationship per line when possible.

  • Prefer clearly named boundaries over dense decoration.

  • Use repeated box shapes for similar components.

Arrow Types

ArrowMeaning
---->Data flow or dependency
<--->Bidirectional connection
- - >Optional or conditional resource

Grouping and Boundaries

Group related components inside a larger boundary when they share a network, account, or deployment domain.

Use a full box for a strong boundary:

+-----------------------------------------------+
|  Resource Group                               |
|                                               |
|  +-------------+        +-------------+       |
|  |   VNet      |------->|   Subnet    |       |
|  +-------------+        +-------------+       |
|                                               |
+-----------------------------------------------+

Use labeled boundaries for secondary or nested boundaries:

:--- Virtual Network ---------------------------:
:                                               :
:  +-------------+        +-------------+       :
:  |   Subnet A  |------->|   Subnet B  |       :
:  +-------------+        +-------------+       :
:                                               :
:-----------------------------------------------:

Mermaid Conventions

When the caller chooses Mermaid output, render a mermaid fenced code block using a flowchart that expresses the same structure, boundaries, and relationships you would draw in ASCII.

  • Use flowchart TB for top-to-bottom topologies and flowchart LR when the main flow reads left to right.

  • Declare each component as a node with a short, specific label, for example lb["Load Balancer"], and use [("...")] for data stores.

  • Group components that share a network, account, or deployment domain inside a subgraph block, such as a VNet, subnet, or resource group.

  • Use --> for data flow or dependency, <--> for bidirectional connections, and -. optional .-> for optional or conditional links.

  • Keep node identifiers stable and lowercase, and reserve labels for the human-readable name.

flowchart TB
    subgraph rg["Resource Group"]
        lb["Load Balancer"]
        subgraph subnet["App Subnet"]
            vm1["VM 1"]
            vm2["VM 2"]
        end
        db[("SQL Database")]
    end
    lb --> vm1
    lb --> vm2
    vm1 --> db
    vm2 --> db

Layout Guidelines

  • Place external or public services at the top.

  • Place compute or application tiers in the middle.

  • Place data stores at the bottom.

  • Group components by network boundary, such as a VNet or subnet.

  • Let the main flow run from top to bottom when the direction is clear.

Resource Identification Heuristics

When reading infrastructure sources, extract:

  • Resource type and name

  • Network associations, including VNet, subnet, private endpoint, or ingress settings

  • Dependencies that are explicit in configuration and those that are implied by references

  • Deployment relationships such as container registry, service mesh, or workload placement

Output Format Contract

Use this structure for every infrastructure diagram:

## <Name> Architecture

[diagram in the selected format]

### Legend
[Arrow meanings from this diagram; reference the arrow types above]

### Key Relationships
[Notable connections and dependencies]

The title should use title case and follow the pattern <Name> Architecture. The legend should explain any special symbols used, and the key relationships section should focus on the most important dependencies or data flows.

Catalog diagrams use the parallel ## <Engagement> Data Model contract defined in catalog-erd.md, with a Legend covering multiplicity and confidence and a Key Relationships section carrying every declared relationship and its basis.

Worked Example: AKS Platform Architecture

## AKS Platform Architecture

+===============================================================+
|  Resource Group                                               |
|  :--- Virtual Network ------------------------------------:   |
|  :  +------------------+        +------------------+      :   |
|  :  |   NAT Gateway    |------->|   AKS Cluster    |      :   |
|  :  +------------------+        +--------+---------+      :   |
|  :                              +--------v---------+      :   |
|  :                              |       ACR        |      :   |
| :                              +------------------+      : |
|:----------------------------------------------------------:|
|      +------------------+        +------------------+      |
|     | Log Analytics    |<-------|  App Insights    |          |
|     +------------------+        +------------------+          |
+===============================================================+

### Legend
See the arrow types above. Additional symbols: `====` primary boundary, `:---:` secondary boundary.

### Key Relationships
* AKS pulls images from ACR through the network boundary.
* NAT Gateway provides egress for AKS workloads.

The same architecture in Mermaid form expresses identical structure, boundaries, and relationships:

## AKS Platform Architecture

```mermaid
flowchart TB
    subgraph rg["Resource Group"]
        subgraph vnet["Virtual Network"]
            nat["NAT Gateway"]
            aks["AKS Cluster"]
            acr["ACR"]
        end
        appinsights["App Insights"]
        logs[("Log Analytics")]
    end
    nat --> aks
    aks --> acr
    appinsights --> logs
```

### Legend
See the arrow types above; `subgraph` blocks denote network or resource boundaries.

### Key Relationships
* AKS pulls images from ACR through the network boundary.
* NAT Gateway provides egress for AKS workloads.

Authoring Guidelines

  • Ask one or two clarifying questions when the architecture scope is ambiguous.

  • Announce the current workflow stage when you move from discovery to parsing or generation.

  • Present a draft diagram with a short summary of the resources included before finalizing.

  • Note important inference decisions, such as implicit dependencies, when they affect the diagram.

  • Treat the diagram as a static representation of infrastructure sources, not a runtime execution view.

  • Keep the output focused on a single architecture scope so it remains readable.

Files

scripts/render_catalog_erd.py

#!/usr/bin/env python3
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
"""Render a declared DS_CATALOG_V1 model as Mermaid or ASCII.

Usage:
    uv run python scripts/render_catalog_erd.py catalog.md --format mermaid
"""

from __future__ import annotations

import argparse
import datetime as dt
import math
import re
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Any

import yaml

EXIT_SUCCESS = 0
EXIT_FAILURE = 1
EXIT_ERROR = 2

CONFIDENCE_VALUES = ("confirmed", "inferred", "assumed")
MINIMUM_VALUES = ("zero", "one")

# Operational bound checked before any catalog content is read.
MAX_INPUT_BYTES = 5 * 1024 * 1024
MERGE_TAG = "tag:yaml.org,2002:merge"

# Output-safety bounds. Every projected display value is rejected at the
# boundary rather than escaped, so no declared value can change the structure
# of the emitted Markdown or Mermaid.
MAX_DISPLAY_TEXT_LENGTH = 120
FORBIDDEN_DISPLAY_SUBSTRINGS = ("```", "%%{", "<", ">", "{", "}", '"')
MERMAID_ATTRIBUTE_TOKEN_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
MERMAID_ATTRIBUTE_TYPE = "string"
EXPECTED_FENCE_COUNT = 2
DEFAULT_HEADING_LEVEL = 2
MAX_HEADING_LEVEL = 5

# Maximum multiplicity for each endpoint, derived from the declared cardinality.
CARDINALITY_MAXIMUMS = {
    "one-to-one": ("one", "one"),
    "one-to-many": ("one", "many"),
    "many-to-many": ("many", "many"),
}

# Mermaid erDiagram notation keyed by (minimum, maximum). The left form mirrors
# the right so each marker points away from the entity it constrains.
MERMAID_LEFT = {
    ("zero", "one"): "|o",
    ("one", "one"): "||",
    ("zero", "many"): "}o",
    ("one", "many"): "}|",
}
MERMAID_RIGHT = {
    ("zero", "one"): "o|",
    ("one", "one"): "||",
    ("zero", "many"): "o{",
    ("one", "many"): "|{",
}
ASCII_MULTIPLICITY = {
    ("zero", "one"): "0..1",
    ("one", "one"): "1",
    ("zero", "many"): "0..*",
    ("one", "many"): "1..*",
}


class CatalogRenderError(ValueError):
    """Raised when a catalog cannot be rendered safely."""


class UniqueKeyLoader(yaml.SafeLoader):
    """Loader rejecting aliases, anchors, tags, merge keys, and duplicate keys."""

    def compose_node(self, parent: yaml.nodes.Node | None, index: Any) -> yaml.nodes.Node:
        """Reject alias, anchor, and explicit-tag events before composition."""
        event = self.peek_event()
        if isinstance(event, yaml.AliasEvent):
            raise CatalogRenderError("catalog YAML aliases are not permitted")
        if getattr(event, "anchor", None) is not None:
            raise CatalogRenderError("catalog YAML anchors are not permitted")
        if getattr(event, "tag", None) is not None:
            raise CatalogRenderError("catalog YAML explicit tags are not permitted")
        return super().compose_node(parent, index)


def _construct_unique_mapping(
    loader: UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False
) -> dict[str, Any]:
    """Construct one mapping while rejecting merge keys and duplicate keys."""
    mapping: dict[str, Any] = {}
    for key_node, value_node in node.value:
        if key_node.tag == MERGE_TAG:
            raise CatalogRenderError("catalog YAML merge keys are not permitted")
        key = loader.construct_object(key_node, deep=deep)
        if not isinstance(key, str):
            raise CatalogRenderError("catalog YAML keys must be strings")
        if key in mapping:
            raise CatalogRenderError(f"duplicate catalog YAML key: {key}")
        mapping[key] = loader.construct_object(value_node, deep=deep)
    return mapping


def _reject_tagged_node(loader: UniqueKeyLoader, tag_suffix: str, node: yaml.nodes.Node) -> Any:
    """Reject any node carrying a tag without a registered safe constructor."""
    raise CatalogRenderError("catalog YAML explicit tags are not permitted")


UniqueKeyLoader.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_unique_mapping
)
UniqueKeyLoader.add_multi_constructor("", _reject_tagged_node)


def _sanitize_yaml_error(error: yaml.YAMLError) -> str:
    """Describe a YAML failure by position only, never by source content."""
    mark = getattr(error, "problem_mark", None) or getattr(error, "context_mark", None)
    if mark is None:
        return f"invalid catalog YAML ({type(error).__name__})"
    return (
        f"invalid catalog YAML ({type(error).__name__}) at line {mark.line + 1} "
        f"column {mark.column + 1}"
    )


def _skill_root() -> Path:
    """Return the skill root that owns this renderer."""
    return Path(__file__).resolve().parent.parent


def _resolve_input_path(path: Path, allowed_roots: Sequence[Path]) -> Path:
    """Return a resolved input path contained by one permitted root."""
    segments = str(path).replace("\\", "/").split("/")
    if any(segment == ".." for segment in segments):
        raise CatalogRenderError("input path cannot contain '..' segments")
    resolved = path.resolve()
    for root in allowed_roots:
        if resolved.is_relative_to(root.resolve()):
            return resolved
    raise CatalogRenderError("input path resolves outside the permitted roots")


def read_catalog_text(path: Path, allowed_roots: Sequence[Path] | None = None) -> str:
    """Read a size-bounded catalog file from a permitted root."""
    roots = tuple(allowed_roots) if allowed_roots else (Path.cwd(), _skill_root())
    resolved = _resolve_input_path(path, roots)
    if resolved.stat().st_size > MAX_INPUT_BYTES:
        raise CatalogRenderError(f"catalog exceeds the {MAX_INPUT_BYTES} byte input limit")
    return resolved.read_text(encoding="utf-8")


def _assert_json_compatible(value: Any, path: str = "$") -> None:
    """Reject YAML-native values outside the JSON data model."""
    if value is None or isinstance(value, (str, bool, int)):
        return
    if isinstance(value, float):
        if math.isnan(value) or math.isinf(value):
            raise CatalogRenderError(f"{path} must be a finite number")
        return
    if isinstance(value, list):
        for index, item in enumerate(value):
            _assert_json_compatible(item, f"{path}[{index}]")
        return
    if isinstance(value, dict):
        for key, item in value.items():
            if not isinstance(key, str):
                raise CatalogRenderError(f"{path} has a non-string key")
            _assert_json_compatible(item, f"{path}.{key}")
        return
    if isinstance(value, dt.date):
        raise CatalogRenderError(f"{path} timestamp must be a quoted string")
    raise CatalogRenderError(f"{path} contains non-JSON YAML value {type(value).__name__}")


def extract_frontmatter(markdown: str) -> str:
    """Extract YAML frontmatter from a Markdown catalog."""
    lines = markdown.splitlines()
    if not lines or lines[0] != "---":
        raise CatalogRenderError("catalog must start with YAML frontmatter")
    try:
        closing = lines.index("---", 1)
    except ValueError as error:
        raise CatalogRenderError("catalog frontmatter is not closed") from error
    return "\n".join(lines[1:closing])


def parse_catalog(markdown: str) -> dict[str, Any]:
    """Parse a catalog and reject every malformed rendering-critical fact."""
    try:
        data = yaml.load(extract_frontmatter(markdown), Loader=UniqueKeyLoader)
    except CatalogRenderError:
        raise
    except yaml.YAMLError as error:
        raise CatalogRenderError(_sanitize_yaml_error(error)) from error
    except ValueError as error:
        raise CatalogRenderError("catalog frontmatter has an invalid scalar value") from error
    if not isinstance(data, dict):
        raise CatalogRenderError("catalog frontmatter must be an object")
    _assert_json_compatible(data)
    if data.get("catalog_version") != "DS_CATALOG_V1":
        raise CatalogRenderError("unsupported catalog_version; expected DS_CATALOG_V1")

    entities = data.get("entities")
    relationships = data.get("relationships")
    if not isinstance(entities, list) or not isinstance(relationships, list):
        raise CatalogRenderError("catalog entities and relationships must be arrays")

    entity_ids: set[str] = set()
    for entity in entities:
        if not isinstance(entity, dict):
            raise CatalogRenderError("every catalog entity must be an object")
        entity_id = _require_nonempty_string(entity.get("id"), "entity id")
        _require_nonempty_string(entity.get("name"), f"entity {entity_id} name")
        if entity_id in entity_ids:
            raise CatalogRenderError(f"duplicate catalog entity id: {entity_id}")
        entity_ids.add(entity_id)

    relationship_ids: set[str] = set()
    for relationship in relationships:
        if not isinstance(relationship, dict):
            raise CatalogRenderError("every catalog relationship must be an object")
        relationship_id = _require_nonempty_string(relationship.get("id"), "relationship id")
        if relationship_id in relationship_ids:
            raise CatalogRenderError(f"duplicate catalog relationship id: {relationship_id}")
        relationship_ids.add(relationship_id)

        if relationship.get("from") not in entity_ids or relationship.get("to") not in entity_ids:
            raise CatalogRenderError(
                f"relationship {relationship_id} endpoints must resolve declared entities"
            )
        if relationship.get("cardinality") not in CARDINALITY_MAXIMUMS:
            raise CatalogRenderError(f"relationship {relationship_id} cardinality is unsupported")
        for side in ("from_minimum", "to_minimum"):
            if relationship.get(side) not in MINIMUM_VALUES:
                raise CatalogRenderError(
                    f"relationship {relationship_id} {side} must be 'zero' or 'one'"
                )
        if relationship.get("confidence") not in CONFIDENCE_VALUES:
            raise CatalogRenderError(f"relationship {relationship_id} confidence is unsupported")
        _require_nonempty_string(relationship.get("basis"), f"relationship {relationship_id} basis")
        _validate_join_keys(relationship.get("join_keys"), relationship_id)
    assert_render_safe(data)
    return data


def _require_nonempty_string(value: Any, description: str) -> str:
    """Return a non-empty string value or raise a render error."""
    if not isinstance(value, str) or not value.strip():
        raise CatalogRenderError(f"{description} must be a non-empty string")
    return value


def _require_display_text(value: Any, description: str) -> str:
    """Return a bounded single-line value safe to place in generated output."""
    text = _require_nonempty_string(value, description)
    if len(text) > MAX_DISPLAY_TEXT_LENGTH:
        raise CatalogRenderError(
            f"{description} exceeds {MAX_DISPLAY_TEXT_LENGTH} display characters"
        )
    if "\n" in text or "\r" in text:
        raise CatalogRenderError(f"{description} must be a single line")
    for forbidden in FORBIDDEN_DISPLAY_SUBSTRINGS:
        if forbidden in text:
            raise CatalogRenderError(f"{description} cannot contain the sequence {forbidden!r}")
    return text


def _require_attribute_token(value: Any, description: str) -> str:
    """Return a value usable as a Mermaid ER attribute token."""
    token = _require_nonempty_string(value, description)
    if MERMAID_ATTRIBUTE_TOKEN_PATTERN.match(token) is None:
        raise CatalogRenderError(f"{description} is not a valid Mermaid attribute token")
    return token


def assert_render_safe(data: dict[str, Any]) -> None:
    """Reject every projected value that could escape its output boundary.

    Runs before any emission. Identifiers are validated before they appear in
    an error message so no unvalidated value reaches an operator log.
    """
    engagement = data.get("engagement")
    if isinstance(engagement, str) and engagement.strip():
        _require_display_text(engagement, "engagement")

    for index, entity in enumerate(data.get("entities", [])):
        entity_id = _require_display_text(entity.get("id"), f"entity[{index}] id")
        _require_display_text(entity.get("name"), f"entity {entity_id} name")

    for index, relationship in enumerate(data.get("relationships", [])):
        relationship_id = _require_display_text(relationship.get("id"), f"relationship[{index}] id")
        _require_display_text(relationship.get("basis"), f"relationship {relationship_id} basis")
        _validate_join_keys(relationship.get("join_keys"), relationship_id)

    _node_ids(data.get("entities", []))


def _validate_join_keys(join_keys: Any, relationship_id: str) -> list[tuple[str, str]]:
    """Return ordered join-key pairs, rejecting any malformed declaration."""
    if not isinstance(join_keys, dict):
        raise CatalogRenderError(f"relationship {relationship_id} join_keys must be an object")
    from_field = join_keys.get("from_field")
    to_field = join_keys.get("to_field")

    if isinstance(from_field, str) and isinstance(to_field, str):
        from_values: list[Any] = [from_field]
        to_values: list[Any] = [to_field]
    elif isinstance(from_field, list) and isinstance(to_field, list):
        from_values = from_field
        to_values = to_field
    else:
        raise CatalogRenderError(
            f"relationship {relationship_id} join keys must both be strings or both be arrays"
        )

    if not from_values or len(from_values) != len(to_values):
        raise CatalogRenderError(
            f"relationship {relationship_id} join keys must be non-empty and of equal length"
        )
    pairs = list(zip(from_values, to_values, strict=True))
    for index, (source, target) in enumerate(pairs):
        _require_attribute_token(source, f"relationship {relationship_id} from_field[{index}]")
        _require_attribute_token(target, f"relationship {relationship_id} to_field[{index}]")
    return pairs


def _safe_identifier(entity_id: str) -> str:
    """Derive a Mermaid-safe identifier by escaping every character outside a-z0-9."""
    escaped = re.sub(r"[^a-z0-9]", lambda match: f"_{ord(match.group()):02x}", entity_id.lower())
    return f"entity_{escaped}"


def _node_ids(entities: Sequence[dict[str, Any]]) -> dict[str, str]:
    """Map every entity ID to a distinct, deterministic Mermaid identifier.

    Two entity IDs that sanitize to the same token are disambiguated by an
    incrementing suffix in declaration order rather than merged into one node.
    """
    assigned: dict[str, str] = {}
    used: set[str] = set()
    for entity in entities:
        entity_id = entity["id"]
        base = _safe_identifier(entity_id)
        token = base
        suffix = 2
        while token in used:
            token = f"{base}_{suffix}"
            suffix += 1
        if MERMAID_ATTRIBUTE_TOKEN_PATTERN.match(token) is None:
            raise CatalogRenderError(f"entity {entity_id} yields an unsafe Mermaid identifier")
        used.add(token)
        assigned[entity_id] = token
    return assigned


def _quote(value: str) -> str:
    """Make a value safe to embed inside a Mermaid double-quoted string."""
    return value.replace('"', "'")


def _endpoints(relationship: dict[str, Any]) -> tuple[tuple[str, str], tuple[str, str]]:
    """Return the (minimum, maximum) pair for each relationship endpoint."""
    from_max, to_max = CARDINALITY_MAXIMUMS[relationship["cardinality"]]
    return (
        (relationship["from_minimum"], from_max),
        (relationship["to_minimum"], to_max),
    )


def _join_key_label(relationship: dict[str, Any]) -> str:
    """Render ordered join-key pairs without implying a key role."""
    pairs = _validate_join_keys(relationship["join_keys"], relationship["id"])
    return ", ".join(f"{source} = {target}" for source, target in pairs)


def _confidence_suffix(confidence: str) -> str:
    """Return the label suffix for a confidence value."""
    return "" if confidence == "confirmed" else f" ({confidence})"


def _heading_level(level: int) -> int:
    """Return a heading level that keeps every emitted heading within Markdown."""
    if not isinstance(level, int) or isinstance(level, bool):
        raise CatalogRenderError("heading level must be an integer")
    if not 1 <= level <= MAX_HEADING_LEVEL:
        raise CatalogRenderError(f"heading level must be between 1 and {MAX_HEADING_LEVEL}")
    return level


def _engagement_name(data: dict[str, Any]) -> str:
    """Return the catalog engagement name used in titles and descriptions."""
    engagement = data.get("engagement")
    return engagement if isinstance(engagement, str) and engagement.strip() else "Catalog"


def _title(data: dict[str, Any], level: int) -> str:
    """Return the catalog output title at the caller-selected depth."""
    return f"{'#' * level} {_engagement_name(data)} Data Model"


def _section(text: str, level: int) -> str:
    """Return a section heading one level below the document title."""
    return f"{'#' * (level + 1)} {text}"


def _entity_lines(data: dict[str, Any]) -> list[str]:
    """Return the Entities section body carrying business names and IDs."""
    return [f"* `{entity['id']}`: {entity['name']}" for entity in data["entities"]]


def _key_relationship_lines(data: dict[str, Any]) -> list[str]:
    """Return the Key Relationships section body."""
    entity_by_id = {entity["id"]: entity for entity in data["entities"]}
    if not data["relationships"]:
        return ["No relationships are declared in this catalog."]

    lines = []
    for relationship in data["relationships"]:
        source = entity_by_id[relationship["from"]]
        target = entity_by_id[relationship["to"]]
        (from_min, from_max), (to_min, to_max) = _endpoints(relationship)
        lines.append(
            f"* `{relationship['id']}`: {source['name']} (`{source['id']}`, "
            f"{ASCII_MULTIPLICITY[from_min, from_max]}) to {target['name']} "
            f"(`{target['id']}`, {ASCII_MULTIPLICITY[to_min, to_max]}); cardinality "
            f"`{relationship['cardinality']}` with minima `{from_min}` and `{to_min}`; "
            f"join keys {_join_key_label(relationship)}; confidence "
            f"`{relationship['confidence']}` because {relationship['basis']}"
        )
    return lines


def _shared_sections(data: dict[str, Any], level: int, legend: list[str]) -> list[str]:
    """Return the Entities, Legend, and Key Relationships sections."""
    lines = ["", _section("Entities", level), ""]
    lines.extend(_entity_lines(data))
    lines.extend(["", _section("Legend", level), ""])
    lines.extend(legend)
    lines.extend(["", _section("Key Relationships", level), ""])
    lines.extend(_key_relationship_lines(data))
    return lines


def _assert_fence_integrity(rendered: str) -> None:
    """Confirm no declared value changed the emitted code-fence structure."""
    if rendered.count("```") != EXPECTED_FENCE_COUNT:
        raise CatalogRenderError("rendered output has an unexpected code-fence count")


def render_mermaid(data: dict[str, Any], heading_level: int = DEFAULT_HEADING_LEVEL) -> str:
    """Render declared entities and relationships as a Mermaid ER diagram."""
    level = _heading_level(heading_level)
    assert_render_safe(data)
    name = _engagement_name(data)
    node_ids = _node_ids(data["entities"])
    lines = [
        _title(data, level),
        "",
        "```mermaid",
        "erDiagram",
        f"    accTitle: {name} Data Model",
        (
            f"    accDescr: Entity relationship diagram of {len(data['entities'])} declared "
            f"entities and {len(data['relationships'])} declared relationships in the "
            f"{name} catalog."
        ),
    ]

    attributes: dict[str, list[str]] = {entity["id"]: [] for entity in data["entities"]}
    for relationship in data["relationships"]:
        for source, target in _validate_join_keys(relationship["join_keys"], relationship["id"]):
            attributes[relationship["from"]].append(source)
            attributes[relationship["to"]].append(target)

    for entity in data["entities"]:
        lines.append(f'    {node_ids[entity["id"]]}["{_quote(entity["name"])}"] {{')
        seen: set[str] = set()
        for field in attributes[entity["id"]]:
            if field not in seen:
                seen.add(field)
                lines.append(f"        {MERMAID_ATTRIBUTE_TYPE} {field}")
        lines.append("    }")

    for relationship in data["relationships"]:
        (from_min, from_max), (to_min, to_max) = _endpoints(relationship)
        notation = f"{MERMAID_LEFT[from_min, from_max]}--{MERMAID_RIGHT[to_min, to_max]}"
        label = _join_key_label(relationship) + _confidence_suffix(relationship["confidence"])
        lines.append(
            f"    {node_ids[relationship['from']]} {notation} "
            f'{node_ids[relationship["to"]]} : "{_quote(label)}"'
        )
    lines.append("```")

    lines.extend(
        _shared_sections(
            data,
            level,
            [
                (
                    "* `||` requires exactly one, `|o` allows zero or one, `}|` requires "
                    "one or many, and `}o` allows zero or many on that side."
                ),
                (
                    "* Attributes list declared join-key field names only. They do not "
                    "declare primary keys, foreign keys, or uniqueness."
                ),
                (
                    "* Labels show the declared join-key pairing. An unmarked label is "
                    "`confirmed`; `(inferred)` and `(assumed)` mark unconfirmed "
                    "relationships."
                ),
                (
                    "* All connectors are solid. Identifying and non-identifying "
                    "semantics are not modelled by this catalog."
                ),
            ],
        )
    )
    rendered = "\n".join(lines) + "\n"
    _assert_fence_integrity(rendered)
    return rendered


def render_ascii(data: dict[str, Any], heading_level: int = DEFAULT_HEADING_LEVEL) -> str:
    """Render declared entities and relationships as compact ASCII."""
    level = _heading_level(heading_level)
    assert_render_safe(data)
    lines = [_title(data, level), "", "```text", "Entities:"]
    for entity in data["entities"]:
        lines.append(f"  [{entity['name']}] ({entity['id']})")

    lines.extend(["", "Relationships:"])
    if data["relationships"]:
        entity_by_id = {entity["id"]: entity for entity in data["entities"]}
        for relationship in data["relationships"]:
            (from_min, from_max), (to_min, to_max) = _endpoints(relationship)
            label = _join_key_label(relationship) + _confidence_suffix(relationship["confidence"])
            source = entity_by_id[relationship["from"]]
            target = entity_by_id[relationship["to"]]
            lines.append(
                f"  [{source['name']}] ({relationship['from']}) "
                f"{ASCII_MULTIPLICITY[from_min, from_max]} --- "
                f"{ASCII_MULTIPLICITY[to_min, to_max]} "
                f"[{target['name']}] ({relationship['to']}) : {label}"
            )
    else:
        lines.append("  No relationships are declared in this catalog.")
    lines.append("```")

    lines.extend(
        _shared_sections(
            data,
            level,
            [
                (
                    "* `1` requires exactly one, `0..1` allows zero or one, `1..*` "
                    "requires one or many, and `0..*` allows zero or many on that side."
                ),
                "* Each multiplicity sits beside the entity it constrains.",
                (
                    "* Join-key pairs show declared field names only. They do not declare "
                    "primary keys, foreign keys, or uniqueness."
                ),
                (
                    "* An unmarked relationship is `confirmed`; `(inferred)` and "
                    "`(assumed)` mark unconfirmed relationships."
                ),
            ],
        )
    )
    rendered = "\n".join(lines) + "\n"
    _assert_fence_integrity(rendered)
    return rendered


def create_parser() -> argparse.ArgumentParser:
    """Create the command-line parser."""
    parser = argparse.ArgumentParser(
        description="Render declared DS_CATALOG_V1 relationships as an ERD"
    )
    parser.add_argument("catalog", type=Path, help="DS_CATALOG_V1 Markdown catalog")
    parser.add_argument(
        "--format",
        choices=("ascii", "mermaid"),
        required=True,
        help="Caller-selected output format",
    )
    parser.add_argument(
        "--heading-level",
        type=int,
        default=DEFAULT_HEADING_LEVEL,
        choices=range(1, MAX_HEADING_LEVEL + 1),
        help="Markdown depth of the rendered title in the host document",
    )
    return parser


def run(
    catalog_path: Path,
    output_format: str,
    heading_level: int = DEFAULT_HEADING_LEVEL,
    allowed_roots: Sequence[Path] | None = None,
) -> int:
    """Read one catalog and print its ERD."""
    try:
        data = parse_catalog(read_catalog_text(catalog_path, allowed_roots))
        rendered = (
            render_mermaid(data, heading_level)
            if output_format == "mermaid"
            else render_ascii(data, heading_level)
        )
    except (OSError, CatalogRenderError) as error:
        print(f"render_catalog_erd: {error}", file=sys.stderr)
        return EXIT_ERROR
    print(rendered, end="")
    return EXIT_SUCCESS


def main() -> int:
    """Run the catalog ERD renderer."""
    args = create_parser().parse_args()
    return run(args.catalog, args.format, args.heading_level)


if __name__ == "__main__":
    sys.exit(main())

Requirements and compatibility

Written for: GitHub Copilot, Claude Code

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Claude Code frontmatter, Needs Python, Needs Node.js

The author says: Works in any chat context where the caller needs an ASCII or Mermaid diagram from infrastructure source files or a declared DS_CATALOG_V1 data catalog

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Microsoft
Publisher
Microsoft (Official)
Source
microsoft/hve-core · .github/skills/hve-core/architecture-diagrams · commit f425e94
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Microsoft or Microsoft.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download architecture-diagrams.zip