Architecture Diagrams
MicrosoftOfficialPublished by Microsoft
- Skill
- Content generator
- Software development
- MIT
- Low risk
- Has scripts
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
Download the skill as a ZIP:
https://autogineer.com/agent-skills/microsoft/architecture-diagrams/downloadUnzip 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_V1input, 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:
Explicit request or caller state.
Root state file at
.copilot-tracking/architecture-diagrams/state.json.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:
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.
Parsing. For infrastructure, extract services, data stores, networking components, ingress points, and deployment units. For a catalog, execute
scripts/render_catalog_erd.pyor follow catalog-erd.md without adding inferred semantics.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.
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
| Arrow | Meaning |
|---|---|
----> | 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 TBfor top-to-bottom topologies andflowchart LRwhen 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
subgraphblock, 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 --> dbLayout 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
- ATTRIBUTION.mdAdded by AutoGineer847 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md11.8 KiB
- pyproject.toml561 B
- references/catalog-erd.md5.0 KiB
- scripts/render_catalog_erd.pyScript25.2 KiB
- tests/corpus/0_valid_catalog157 B
- tests/corpus/1_empty_frontmatter8 B
- tests/corpus/2_unclosed_sequence20 B
- tests/corpus/3_scalar_entities29 B
- tests/corpus/4_no_frontmatter15 B
- tests/corpus/README.md1.2 KiB
- tests/fuzz_harness.pyScript1.1 KiB
- tests/test_render_catalog_erd.pyScript24.8 KiB
- uv.lock67.0 KiB
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 or Microsoft.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download architecture-diagrams.zip