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://mp-staging.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

tests/test_render_catalog_erd.py

# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
"""Tests for catalog-driven ERD rendering."""

from __future__ import annotations

import copy
import re
from pathlib import Path
from typing import Any

import pytest
import render_catalog_erd as render_catalog_erd_module
import yaml
from render_catalog_erd import (
    ASCII_MULTIPLICITY,
    MAX_DISPLAY_TEXT_LENGTH,
    MERMAID_ATTRIBUTE_TOKEN_PATTERN,
    MERMAID_ATTRIBUTE_TYPE,
    CatalogRenderError,
    _node_ids,
    _sanitize_yaml_error,
    create_parser,
    main,
    parse_catalog,
    read_catalog_text,
    render_ascii,
    render_mermaid,
    run,
)

REPO_ROOT = Path(__file__).resolve().parents[5]
CATALOG_FIXTURE = (
    REPO_ROOT
    / ".github"
    / "skills"
    / "data-science-engineering"
    / "data-catalog"
    / "examples"
    / "northwind-catalog.md"
)


def _catalog() -> dict:
    return copy.deepcopy(parse_catalog(CATALOG_FIXTURE.read_text(encoding="utf-8")))


def _to_markdown(data: dict) -> str:
    """Serialize a catalog dictionary into Markdown frontmatter."""
    return f"---\n{yaml.safe_dump(data, sort_keys=False)}---\n"


def _minimal(**overrides: Any) -> dict:
    """Build a two-entity catalog with one overridable relationship."""
    relationship: dict[str, Any] = {
        "id": "rel-a-b",
        "from": "alpha",
        "to": "beta",
        "cardinality": "one-to-many",
        "from_minimum": "one",
        "to_minimum": "zero",
        "join_keys": {"from_field": "alpha_id", "to_field": "alpha_id"},
        "confidence": "confirmed",
        "basis": "Confirmed by the data owner",
    }
    relationship.update(overrides)
    return {
        "catalog_version": "DS_CATALOG_V1",
        "engagement": "demo",
        "entities": [
            {"id": "alpha", "name": "Alpha"},
            {"id": "beta", "name": "Beta"},
        ],
        "relationships": [relationship],
    }


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_catalog_when_rendered_then_document_sections_are_present(
    renderer,
) -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = renderer(data)

    # Assert
    assert rendered.startswith("## northwind-modernization Data Model")
    assert "### Legend" in rendered
    assert "### Key Relationships" in rendered


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_catalog_when_rendered_then_every_declared_fact_survives(
    renderer,
) -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = renderer(data)

    # Assert
    for entity in data["entities"]:
        assert entity["name"] in rendered
    for relationship in data["relationships"]:
        assert relationship["id"] in rendered
        assert relationship["basis"] in rendered


def test_given_catalog_when_rendered_as_mermaid_then_uses_er_diagram() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert "```mermaid" in rendered
    assert "erDiagram" in rendered
    assert "flowchart" not in rendered


def test_given_catalog_when_rendered_as_ascii_then_no_mermaid_block() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_ascii(data)

    # Assert
    assert "```mermaid" not in rendered
    assert "```text" in rendered


@pytest.mark.parametrize(
    ("cardinality", "from_minimum", "to_minimum", "notation"),
    [
        ("one-to-one", "one", "one", "||--||"),
        ("one-to-one", "zero", "zero", "|o--o|"),
        ("one-to-many", "one", "zero", "||--o{"),
        ("one-to-many", "zero", "one", "|o--|{"),
        ("many-to-many", "one", "one", "}|--|{"),
        ("many-to-many", "zero", "zero", "}o--o{"),
    ],
)
def test_given_multiplicity_when_rendered_as_mermaid_then_notation_matches(
    cardinality: str, from_minimum: str, to_minimum: str, notation: str
) -> None:
    # Arrange
    data = _minimal(
        cardinality=cardinality,
        from_minimum=from_minimum,
        to_minimum=to_minimum,
    )

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert f"entity_alpha {notation} entity_beta" in rendered


@pytest.mark.parametrize(
    ("cardinality", "from_minimum", "to_minimum", "notation"),
    [
        ("one-to-one", "one", "one", "1 --- 1"),
        ("one-to-one", "zero", "zero", "0..1 --- 0..1"),
        ("one-to-many", "one", "zero", "1 --- 0..*"),
        ("many-to-many", "zero", "one", "0..* --- 1..*"),
    ],
)
def test_given_multiplicity_when_rendered_as_ascii_then_notation_matches(
    cardinality: str, from_minimum: str, to_minimum: str, notation: str
) -> None:
    # Arrange
    data = _minimal(
        cardinality=cardinality,
        from_minimum=from_minimum,
        to_minimum=to_minimum,
    )

    # Act
    rendered = render_ascii(data)

    # Assert
    assert notation in rendered


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_confirmed_relationship_when_rendered_then_label_is_unmarked(
    renderer,
) -> None:
    # Arrange
    data = _minimal(confidence="confirmed")

    # Act
    rendered = renderer(data)

    # Assert
    assert "(confirmed)" not in rendered
    assert "alpha_id = alpha_id" in rendered


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
@pytest.mark.parametrize("confidence", ["inferred", "assumed"])
def test_given_unconfirmed_relationship_when_rendered_then_label_is_suffixed(
    renderer, confidence: str
) -> None:
    # Arrange
    data = _minimal(confidence=confidence)

    # Act
    rendered = renderer(data)

    # Assert
    assert f"alpha_id = alpha_id ({confidence})" in rendered


def test_given_composite_keys_when_rendered_then_order_is_preserved() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert "tenant_id = tenant_id, customer_id = customer_id" in rendered


def test_given_scalar_keys_when_rendered_then_pairing_is_visible() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_ascii(data)

    # Assert
    assert "customer_id = account_ref" in rendered


def test_given_catalog_when_rendered_as_mermaid_then_keys_are_role_neutral() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert "catalog_id" not in rendered
    assert " PK" not in rendered
    assert " FK" not in rendered
    assert "        string tenant_id" in rendered


def test_given_repeated_join_key_when_rendered_then_attribute_is_deduplicated() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)
    block = rendered.split('entity_sales_2dorder_2dline["Sales Order Line"]')[1]
    block = block.split("    }")[0]

    # Assert
    assert block.count("string tenant_id") == 1


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_no_relationships_when_rendered_then_entities_and_notice_appear(
    renderer,
) -> None:
    # Arrange
    data = _minimal()
    data["relationships"] = []

    # Act
    rendered = renderer(data)

    # Assert
    assert "Alpha" in rendered
    assert "Beta" in rendered
    assert "No relationships are declared in this catalog." in rendered


def test_given_unknown_endpoint_when_parsed_then_raises() -> None:
    # Arrange
    markdown = CATALOG_FIXTURE.read_text(encoding="utf-8").replace(
        "to: sales-order-line", "to: missing-entity"
    )

    # Act and assert
    with pytest.raises(CatalogRenderError, match="endpoints"):
        parse_catalog(markdown)


def test_given_unsupported_version_when_parsed_then_raises() -> None:
    # Arrange
    markdown = CATALOG_FIXTURE.read_text(encoding="utf-8").replace(
        "catalog_version: DS_CATALOG_V1", "catalog_version: DS_CATALOG_V2"
    )

    # Act and assert
    with pytest.raises(CatalogRenderError, match="unsupported catalog_version"):
        parse_catalog(markdown)


def test_given_missing_frontmatter_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="must start"):
        parse_catalog("# Catalog\n")


def test_given_unclosed_frontmatter_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="not closed"):
        parse_catalog("---\ncatalog_version: DS_CATALOG_V1\n")


def test_given_duplicate_yaml_key_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="duplicate catalog YAML key"):
        parse_catalog("---\ncatalog_version: DS_CATALOG_V1\ncatalog_version: other\n---\n")


@pytest.mark.parametrize(
    ("overrides", "message"),
    [
        ({"cardinality": "one-to-some"}, "cardinality is unsupported"),
        ({"confidence": "likely"}, "confidence is unsupported"),
        ({"basis": "  "}, "basis must be a non-empty string"),
        ({"id": ""}, "relationship id must be a non-empty string"),
        ({"from_minimum": "maybe"}, "from_minimum must be"),
        ({"to_minimum": 0}, "to_minimum must be"),
    ],
)
def test_given_malformed_relationship_when_parsed_then_raises(
    overrides: dict, message: str
) -> None:
    # Arrange
    data = _minimal(**overrides)

    # Act and assert
    with pytest.raises(CatalogRenderError, match=message):
        parse_catalog(_to_markdown(data))


@pytest.mark.parametrize(
    ("join_keys", "message"),
    [
        ({"from_field": "a", "to_field": ["a"]}, "must both be strings"),
        ({"from_field": [], "to_field": []}, "non-empty"),
        ({"from_field": ["a", "b"], "to_field": ["a"]}, "equal length"),
        ({"from_field": ["a", ""], "to_field": ["a", "b"]}, r"from_field\[1\]"),
        ({"from_field": ["a"], "to_field": [5]}, r"to_field\[0\]"),
        ("not-an-object", "join_keys must be an object"),
    ],
)
def test_given_malformed_join_keys_when_parsed_then_raises(join_keys: Any, message: str) -> None:
    # Arrange
    data = _minimal(join_keys=join_keys)

    # Act and assert
    with pytest.raises(CatalogRenderError, match=message):
        parse_catalog(_to_markdown(data))


def test_given_missing_endpoint_minimum_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    del data["relationships"][0]["to_minimum"]

    # Act and assert
    with pytest.raises(CatalogRenderError, match="to_minimum must be"):
        parse_catalog(_to_markdown(data))


def test_given_duplicate_entity_id_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"].append({"id": "alpha", "name": "Alpha Again"})

    # Act and assert
    with pytest.raises(CatalogRenderError, match="duplicate catalog entity id"):
        parse_catalog(_to_markdown(data))


def test_given_duplicate_relationship_id_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["relationships"].append(copy.deepcopy(data["relationships"][0]))

    # Act and assert
    with pytest.raises(CatalogRenderError, match="duplicate catalog relationship id"):
        parse_catalog(_to_markdown(data))


def test_given_colliding_entity_ids_when_parsed_then_identifiers_stay_distinct() -> None:
    # Arrange
    data = _minimal()
    data["entities"].append({"id": "Alpha", "name": "Upper Alpha"})

    # Act
    rendered = render_mermaid(parse_catalog(_to_markdown(data)))

    # Assert
    assert 'entity_alpha["Alpha"]' in rendered
    assert 'entity_alpha_2["Upper Alpha"]' in rendered


def test_given_entity_without_name_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0].pop("name")

    # Act and assert
    with pytest.raises(CatalogRenderError, match="entity alpha name"):
        parse_catalog(_to_markdown(data))


def test_given_non_object_frontmatter_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="must be an object"):
        parse_catalog("---\n- one\n- two\n---\n")


def test_given_non_array_entities_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="must be arrays"):
        parse_catalog("---\ncatalog_version: DS_CATALOG_V1\nentities: {}\nrelationships: []\n---\n")


@pytest.mark.parametrize("output_format", ["mermaid", "ascii"])
def test_given_valid_fixture_when_run_then_returns_success(
    output_format: str, capsys: pytest.CaptureFixture[str]
) -> None:
    # Act
    result = run(CATALOG_FIXTURE, output_format, allowed_roots=(REPO_ROOT,))

    # Assert
    assert result == 0
    assert "Data Model" in capsys.readouterr().out


def test_given_missing_file_when_run_then_returns_error(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    # Act
    result = run(tmp_path / "missing.md", "mermaid", allowed_roots=(tmp_path,))

    # Assert
    assert result == 2
    assert "render_catalog_erd:" in capsys.readouterr().err


def test_given_malformed_catalog_when_run_then_errors_without_output(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    # Arrange
    path = tmp_path / "catalog.md"
    path.write_text(_to_markdown(_minimal(from_minimum="maybe")), encoding="utf-8")

    # Act
    result = run(path, "mermaid", allowed_roots=(tmp_path,))

    # Assert
    captured = capsys.readouterr()
    assert result == 2
    assert captured.out == ""
    assert "from_minimum must be" in captured.err


@pytest.mark.parametrize("candidate", ["../evil.md", "..\\evil.md", "a/../../evil.md"])
def test_given_traversal_path_when_read_then_raises(candidate: str) -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match=r"'\.\.' segments"):
        read_catalog_text(Path(candidate))


def test_given_traversal_path_when_run_then_returns_operational_error(
    capsys: pytest.CaptureFixture[str],
) -> None:
    # Act
    result = run(Path("../evil.md"), "mermaid")

    # Assert
    assert result == 2
    assert "'..' segments" in capsys.readouterr().err


def test_given_path_outside_root_when_read_then_raises(tmp_path: Path) -> None:
    # Arrange
    inside = tmp_path / "inside"
    inside.mkdir()
    outside = tmp_path / "outside.md"
    outside.write_text(_to_markdown(_minimal()), encoding="utf-8")

    # Act and assert
    with pytest.raises(CatalogRenderError, match="outside the permitted roots"):
        read_catalog_text(outside, allowed_roots=(inside,))


def test_given_symlink_outside_root_when_read_then_raises(tmp_path: Path) -> None:
    # Arrange
    inside = tmp_path / "inside"
    inside.mkdir()
    outside = tmp_path / "outside.md"
    outside.write_text(_to_markdown(_minimal()), encoding="utf-8")
    link = inside / "link.md"
    try:
        link.symlink_to(outside)
    except (OSError, NotImplementedError):
        pytest.skip("symlink creation is not permitted in this environment")

    # Act and assert
    with pytest.raises(CatalogRenderError, match="outside the permitted roots"):
        read_catalog_text(link, allowed_roots=(inside,))


def test_given_oversized_file_when_read_then_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Arrange
    path = tmp_path / "catalog.md"
    path.write_text(_to_markdown(_minimal()), encoding="utf-8")
    monkeypatch.setattr(render_catalog_erd_module, "MAX_INPUT_BYTES", 4)

    # Act and assert
    with pytest.raises(CatalogRenderError, match="byte input limit"):
        read_catalog_text(path, allowed_roots=(tmp_path,))


@pytest.mark.parametrize(
    ("body", "message"),
    [
        ("copy: *undefined\n", "aliases are not permitted"),
        ("base: &anchor value\n", "anchors are not permitted"),
        ("base: &anchor value\ncopy: *anchor\n", "not permitted"),
        ("value: !custom scalar\n", "explicit tags are not permitted"),
        ("merged:\n  <<: {a: 1}\n", "merge keys are not permitted"),
        ("catalog_version: DS_CATALOG_V2\n", "duplicate catalog YAML key"),
    ],
    ids=["alias", "anchor", "alias-graph", "tag", "merge-key", "duplicate-key"],
)
def test_given_unsafe_yaml_construct_when_parsed_then_raises(body: str, message: str) -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match=message):
        parse_catalog(f"---\ncatalog_version: DS_CATALOG_V1\n{body}---\n")


def test_given_unquoted_date_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="must be a quoted string"):
        parse_catalog("---\ncatalog_version: DS_CATALOG_V1\ngenerated_at: 2026-01-01\n---\n")


@pytest.mark.parametrize("timestamp", ["2026-02-31", "2026-13-01", "2026-01-32"])
def test_given_out_of_range_timestamp_when_parsed_then_raises(timestamp: str) -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="invalid scalar value"):
        parse_catalog(f"---\ncatalog_version: DS_CATALOG_V1\ngenerated_at: {timestamp}\n---\n")


def test_given_non_finite_number_when_parsed_then_raises() -> None:
    # Act and assert
    with pytest.raises(CatalogRenderError, match="must be a finite number"):
        parse_catalog("---\ncatalog_version: DS_CATALOG_V1\nthreshold: .nan\n---\n")


def test_given_quoted_markdown_prose_when_parsed_then_value_survives() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "Alpha ledger, revised 2026-01-01 (see notes)"

    # Act
    parsed = parse_catalog(_to_markdown(data))

    # Assert
    assert parsed["entities"][0]["name"] == "Alpha ledger, revised 2026-01-01 (see notes)"


def test_given_parser_error_when_run_then_output_excludes_source(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    # Arrange
    path = tmp_path / "catalog.md"
    path.write_text(
        '---\ncatalog_version: "unterminated\ncustomer_secret_value: 42\n---\n',
        encoding="utf-8",
    )

    # Act
    result = run(path, "mermaid", allowed_roots=(tmp_path,))

    # Assert
    captured = capsys.readouterr()
    assert result == 2
    assert "invalid catalog YAML" in captured.err
    assert "customer_secret_value" not in captured.err


def test_given_unmarked_yaml_error_when_sanitized_then_reports_type_only() -> None:
    # Act
    message = _sanitize_yaml_error(yaml.YAMLError("customer secret detail"))

    # Assert
    assert message == "invalid catalog YAML (YAMLError)"


def test_given_newline_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "Alpha\n## Injected heading"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="must be a single line"):
        parse_catalog(_to_markdown(data))


def test_given_code_fence_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "Alpha ``` injected"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="cannot contain the sequence"):
        parse_catalog(_to_markdown(data))


def test_given_mermaid_directive_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "%%{init: {'theme':'dark'}}%%"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="cannot contain the sequence"):
        parse_catalog(_to_markdown(data))


def test_given_script_tag_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "Alpha <script>alert(1)</script>"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="cannot contain the sequence"):
        parse_catalog(_to_markdown(data))


def test_given_mdx_expression_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["engagement"] = "demo {process.env.SECRET}"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="cannot contain the sequence"):
        parse_catalog(_to_markdown(data))


def test_given_quote_in_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = 'Alpha" : "escaped'

    # Act and assert
    with pytest.raises(CatalogRenderError, match="cannot contain the sequence"):
        parse_catalog(_to_markdown(data))


def test_given_over_length_display_text_when_parsed_then_raises() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "A" * (MAX_DISPLAY_TEXT_LENGTH + 1)

    # Act and assert
    with pytest.raises(CatalogRenderError, match="display characters"):
        parse_catalog(_to_markdown(data))


def test_given_bounded_display_text_when_parsed_then_accepted() -> None:
    # Arrange
    data = _minimal()
    data["entities"][0]["name"] = "A" * MAX_DISPLAY_TEXT_LENGTH

    # Act
    parsed = parse_catalog(_to_markdown(data))

    # Assert
    assert parsed["entities"][0]["name"] == "A" * MAX_DISPLAY_TEXT_LENGTH


@pytest.mark.parametrize("field", ["from_field", "to_field"])
def test_given_malformed_attribute_token_when_parsed_then_raises(field: str) -> None:
    # Arrange
    join_keys = {"from_field": "alpha_id", "to_field": "alpha_id"}
    join_keys[field] = "alpha id; DROP"

    # Act and assert
    with pytest.raises(CatalogRenderError, match="Mermaid attribute token"):
        parse_catalog(_to_markdown(_minimal(join_keys=join_keys)))


def test_given_declared_attributes_when_rendered_then_tokens_are_safe() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert MERMAID_ATTRIBUTE_TOKEN_PATTERN.match(MERMAID_ATTRIBUTE_TYPE)
    for line in rendered.splitlines():
        if line.startswith(f"        {MERMAID_ATTRIBUTE_TYPE} "):
            assert MERMAID_ATTRIBUTE_TOKEN_PATTERN.match(line.split()[1])


def test_given_entities_when_identifiers_derived_then_pattern_is_enforced() -> None:
    # Arrange
    entities = [{"id": "a-b"}, {"id": "a_b"}, {"id": "A B"}]

    # Act
    assigned = _node_ids(entities)

    # Assert
    assert len(set(assigned.values())) == len(entities)
    for token in assigned.values():
        assert MERMAID_ATTRIBUTE_TOKEN_PATTERN.match(token)


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_catalog_when_rendered_then_fence_count_is_fixed(renderer) -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = renderer(data)

    # Assert
    assert rendered.count("```") == 2


def test_given_catalog_when_rendered_as_mermaid_then_accessibility_is_declared() -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = render_mermaid(data)

    # Assert
    assert "    accTitle: northwind-modernization Data Model" in rendered
    assert re.search(r"^    accDescr: \S.*\.$", rendered, re.MULTILINE)


def test_given_catalog_when_rendered_then_both_formats_carry_the_same_facts() -> None:
    # Arrange
    data = _catalog()
    facts: list[str] = []
    for entity in data["entities"]:
        facts.extend([entity["id"], entity["name"]])
    for relationship in data["relationships"]:
        facts.extend(
            [
                relationship["id"],
                relationship["from"],
                relationship["to"],
                relationship["cardinality"],
                relationship["from_minimum"],
                relationship["to_minimum"],
                relationship["confidence"],
                relationship["basis"],
            ]
        )
        for side in ("from_field", "to_field"):
            value = relationship["join_keys"][side]
            facts.extend(value if isinstance(value, list) else [value])

    # Act
    mermaid = render_mermaid(data)
    ascii_text = render_ascii(data)

    # Assert
    for fact in facts:
        assert fact in mermaid
        assert fact in ascii_text
    for multiplicity in ASCII_MULTIPLICITY.values():
        assert (multiplicity in mermaid) == (multiplicity in ascii_text)


@pytest.mark.parametrize("renderer", [render_mermaid, render_ascii])
def test_given_heading_level_when_rendered_then_headings_shift(renderer) -> None:
    # Arrange
    data = _catalog()

    # Act
    rendered = renderer(data, 3)

    # Assert
    assert rendered.startswith("### northwind-modernization Data Model")
    assert "#### Entities" in rendered
    assert "#### Legend" in rendered
    assert "#### Key Relationships" in rendered


@pytest.mark.parametrize("level", [0, 6, "2", True])
def test_given_unsupported_heading_level_when_rendered_then_raises(level: Any) -> None:
    # Arrange
    data = _minimal()

    # Act and assert
    with pytest.raises(CatalogRenderError, match="heading level must be"):
        render_mermaid(data, level)


def test_given_cli_arguments_when_parsed_then_defaults_are_applied() -> None:
    # Act
    args = create_parser().parse_args(["catalog.md", "--format", "ascii"])

    # Assert
    assert args.catalog == Path("catalog.md")
    assert args.format == "ascii"
    assert args.heading_level == 2


def test_given_cli_invocation_when_main_runs_then_renders_the_fixture(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    # Arrange
    monkeypatch.chdir(REPO_ROOT)
    monkeypatch.setattr(
        "sys.argv",
        ["render_catalog_erd.py", str(CATALOG_FIXTURE), "--format", "mermaid"],
    )

    # Act
    result = main()

    # Assert
    assert result == 0
    assert "erDiagram" in capsys.readouterr().out

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.

Download ZIP

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