Generated banner for Customer Card Render
Generated logo for Customer Card Render

Customer Card Render

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Content generator
  • Documents and content
  • MIT
  • Low risk
  • Has scripts

From the author

Generate customer-card slide content YAML from Design Thinking canonical artifacts
— the description by Microsoft

AutoGineer summary

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

The customer-card-render skill automates the transformation of canonical Design Thinking markdown files into template-driven content.yaml files. It acts as a mapping layer that extracts specific sections from artifacts like vision statements, problem statements, scenarios, personas, and use cases, organizing them into structured slide content. This tool is designed to be renderer-neutral, producing reviewable YAML files that can be consumed by downstream presentation authoring tools. It does not handle the visual rendering or styling of the final presentation, focusing instead on data extraction and content organization.

What it does

  • Parses canonical Design Thinking markdown files for structured content.
  • Maps extracted sections to predefined content.yaml templates.
  • Supports multiple artifact types including Vision, Problem, Scenario, Persona, and Use Case.
  • Splits complex Use Case artifacts into a structured 4-slide layout.
  • Normalizes text content to ensure consistent formatting in generated YAML.
  • Provides a CLI for batch processing of project-specific artifacts.

When to use it

  • When you need to convert raw Design Thinking markdown documentation into structured slide content.
  • When preparing content for automated presentation generation workflows.
  • When you require a consistent, template-driven structure for customer-facing cards.
  • When integrating with Design Thinking coaching agents to automate slide content creation.

Advantages

  • Decouples content mapping logic from presentation rendering tools.
  • Ensures consistent slide structure across different Design Thinking artifacts.
  • Produces renderer-neutral, reviewable YAML artifacts.
  • Automates the tedious task of manual slide content population.

Usage examples

“Generate slide content for the current project's Design Thinking artifacts.”
The script processes the markdown files in the canonical directory and outputs structured content.yaml files to the designated output folder.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/microsoft/customer-card-render/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip customer-card-render.zip -d .agents/skills

Documentation by Microsoft

Customer Card Render Skill

Converts canonical Design Thinking markdown artifacts into template-driven content.yaml slide definitions.

Overview

This skill handles the Design Thinking-specific mapping layer: extracting sections from canonical markdown artifacts and filling template-driven content.yaml files. It does not build, theme, export, or validate a presentation file. The separate hve-slides skill owns HTML presentation authoring and rendering.

Keeping this layer separate means:

  • Customer-card mapping logic stays independent from any presentation tooling.

  • The skill can be included in packages independently.

  • The generated YAML stays a reviewable, renderer-neutral artifact.

Prerequisites

  • Python 3.11+

  • uv package manager, installed with a package manager that verifies the download against a published hash:

    # Windows
    winget install --id astral-sh.uv -e
    
    # macOS / Linux (Homebrew)
    brew install uv

    To install from a release archive instead, download it from the uv releases page and check it against its .sha256 file before extracting.

Directory Structure

.github/skills/experimental/customer-card-render/
├── SKILL.md
├── pyproject.toml
├── references/
│   └── mapping-spec.md
├── scripts/
│   └── generate_cards.py
├── templates/
│   ├── global-style.yaml
│   ├── persona.content.yaml
│   ├── problem.content.yaml
│   ├── scenario.content.yaml
│   ├── use-case-slide1.content.yaml
│   ├── use-case-slide2.content.yaml
│   ├── use-case-slide3.content.yaml
│   └── vision.content.yaml
└── tests/
    ├── fuzz_harness.py
    └── test_generate_cards.py

Supported Artifact Types

Artifact TypeSlide Layout
Vision StatementSingle slide
Problem StatementSingle slide
ScenarioSingle slide
Use Case4 slides (see below)
PersonaSingle slide
Use Case 4-Slide Layout

Each Use Case expands into 4 consecutive slides with distinct sections:

SlideContent
Slide 1Use Case Description, Use Case Overview, Business Value, Primary User
Slide 2Secondary User, Preconditions, Steps, Data Requirements
Slide 3Equipment Requirements, Operating Environment, Success Criteria, Pain Points
Slide 4Extensions, Evidence

Cards are ordered by artifact type (Vision → Problem → Scenario → Use Case → Persona), then alphabetically by title within each type. Use Cases appear with all 4 slides consecutive (Slide N, N+1, N+2, N+3).

Generate Slide YAML

Generate slide YAML from canonical markdown
python "<customer-card-render-skill-root>/scripts/generate_cards.py" \
  --canonical-dir .copilot-tracking/dt/<project-slug>/canonical \
  --output-dir .copilot-tracking/dt/<project-slug>/render/content

Resolve <customer-card-render-skill-root> from the loaded skill location before running the command.

generate_cards.py CLI Reference
FlagRequiredDefaultDescription
--canonical-dirNo<skill-root>/canonicalDirectory containing canonical DT markdown files
--output-dirNo<skill-root>/scripts/contentDirectory to write generated content.yaml files
-v, --verboseNo—Enable debug-level logging

The script reads each markdown file in --canonical-dir, detects the artifact type from frontmatter, extracts required sections, and generates content.yaml files. Vision, Problem, Scenario, and Persona artifacts produce one slide each. Use Case artifacts produce 4 consecutive slides per use case.

For the section-to-field mapping contract and Use Case 4-slide layout details, see references/mapping-spec.md.

The generated content.yaml files are this skill's final output. For an explicitly requested HTML presentation, hand reviewed YAML to hve-slides authoring. For a levelled demo, use hve-demo-material and its references/slide-content.md adapter contract. Resolve skills by name, preserve canonical order, source labels and unknown-knowledge markers, and never claim a presentation exists until rendered and validated. This mapping skill does not synthesize narration or dispatch a deck subagent.

DT Coach Integration

The dt-coaching-foundation skill's canonical-deck reference provides opt-in workflow integration for the Design Thinking coaching agent. When a user opts in, the coaching agent offers to generate customer-card YAML at method exit points. The generation command above runs as part of that workflow with --canonical-dir and --output-dir resolved from the active DT project slug in .copilot-tracking/dt/.

Canonical artifacts are produced by the DT coach and live under .copilot-tracking/dt/<project-slug>/canonical/.

Running Tests

cd "<customer-card-render-skill-root>"
uv sync --group dev
uv run pytest tests/

Tests cover parsing, template selection, YAML emission, and regressions. The tests/fuzz_harness.py file is an Atheris polyglot fuzz harness for OSSF Scorecard compliance.

Content Fidelity Note: Use Case Cards

Use Case cards are split across 3 opinionated slides, each with dedicated sections:

  • Slide 1: Introduces the use case with Description, Overview, Business Value, and Primary User

  • Slide 2: Details execution with Secondary User, Preconditions, Steps, and Data Requirements

  • Slide 3: Captures quality criteria with Equipment Requirements, Operating Environment, Success Criteria, Pain Points, and Evidence

This structure ensures all 16 Use Case sections fit legibly across 4 slides without compression. Each section appears in its own textbox with appropriate styling and heading.

For complete mapping details, see references/mapping-spec.md.

Troubleshooting

IssueCauseSolution
uv not founduv not installedRun winget install --id astral-sh.uv -e (Windows) or brew install uv (macOS/Linux)
Python not found by uvNo Python 3.11+ on PATHRun uv python install 3.11
Template not found--canonical-dir contains unknown typeCheck frontmatter type: field against supported artifact types
Empty output directoryNo canonical markdown files foundConfirm --canonical-dir path and that files have --- frontmatter

Files

tests/test_generate_cards.py

# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT

from __future__ import annotations

import importlib.util
import sys
import unittest.mock
from pathlib import Path


def _load_module():
    script_path = Path(__file__).resolve().parents[1] / "scripts" / "generate_cards.py"
    spec = importlib.util.spec_from_file_location("generate_cards", script_path)
    module = importlib.util.module_from_spec(spec)
    assert spec is not None and spec.loader is not None
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def test_extract_section_parses_markdown_block() -> None:
    module = _load_module()
    body = "## Scenario\n\n### Description\nAlpha\n\n### How Might We\nBeta\n"
    assert module.extract_section(body, "Description") == "Alpha"
    assert module.extract_section(body, "How Might We") == "Beta"


def test_template_selection_for_supported_card_types() -> None:
    module = _load_module()
    assert module.template_for_type("Vision Statement").name == "vision.content.yaml"
    assert module.template_for_type("Problem Statement").name == "problem.content.yaml"
    assert module.template_for_type("Scenario").name == "scenario.content.yaml"
    assert module.template_for_type("Use Case").name == "use-case-slide1.content.yaml"
    assert module.template_for_type("Persona").name == "persona.content.yaml"


def test_emit_content_yaml_shape(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    canonical.mkdir(parents=True)
    (canonical / "vision-statement.md").write_text(
        "---\ntitle: Vision Statement\n---\n\n"
        "## Vision Statement\nA customer-ready vision.",
        encoding="utf-8",
    )

    cards = module.collect_cards(canonical)
    output_dir = tmp_path / "render" / "content"
    module.write_outputs(cards, output_dir)

    rendered = (output_dir / "slide-001" / "content.yaml").read_text(encoding="utf-8")
    assert "slide:" in rendered
    assert "elements:" in rendered
    assert "{{TITLE}}" not in rendered


def test_regression_body_max_does_not_raise(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    (canonical / "scenarios").mkdir(parents=True)
    (canonical / "scenarios" / "scenario-a.md").write_text(
        "---\ntitle: Scenario A\n---\n\n## Scenario A\n\n"
        "### Description\nA\n\n"
        "### Scenario Narrative\nB\n\n"
        "### How Might We\nC\n",
        encoding="utf-8",
    )

    cards = module.collect_cards(canonical)
    output_dir = tmp_path / "render" / "content"
    module.write_outputs(cards, output_dir)

    assert (output_dir / "slide-001" / "content.yaml").exists()


def test_regression_t_s_escaped_in_rendered_yaml() -> None:
    module = _load_module()
    card = module.Card(
        artifact_type="Vision Statement",
        title='A "quoted" title',
        summary='Summary with "quotes" and newline\\nnext',
        source_path="vision-statement.md",
        last_updated="2026-04-21",
    )
    rendered = module.render_slide(card, 1)
    assert '\\"quoted\\"' in rendered
    assert "{{TITLE}}" not in rendered


def test_yaml_escape_encodes_list_newlines() -> None:
    module = _load_module()
    raw = "- first item\n- second item\n- third item"
    escaped = module.yaml_escape(raw)
    assert escaped == "- first item\\n- second item\\n- third item"


def test_yaml_escape_unwraps_hard_wrapped_prose() -> None:
    module = _load_module()
    raw = (
        "Enable shift-based operations teams to hand off maintenance issues "
        "as complete,\n"
        "actionable work so the incoming shift can recognize urgency, "
        "understand context,\n"
        "and continue follow-up without re-diagnosing the issue."
    )
    escaped = module.yaml_escape(raw)
    assert "\\n" not in escaped
    assert "complete, actionable work" in escaped


def test_real_section_content_flows_into_rendered_cards(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    (canonical / "scenarios").mkdir(parents=True)
    (canonical / "personas").mkdir(parents=True)

    (canonical / "vision-statement.md").write_text(
        "---\n"
        'title: "Rental Gap Finder - Vision"\n'
        'date: "2026-04-21"\n'
        "---\n\n"
        "## Vision Statement\n\n"
        "A clear operational back office.\n\n"
        "### Why This Matters\n\n"
        "Context reconstruction is expensive.\n",
        encoding="utf-8",
    )

    (canonical / "scenarios" / "maintenance-scramble.md").write_text(
        "---\n"
        'title: "Maintenance Scramble"\n'
        "---\n\n"
        "### Description\n\n"
        "Tenant reports sink issue at 9pm.\n\n"
        "### Scenario Narrative\n\n"
        "Landlord searches across tools before responding.\n\n"
        "### How Might We\n\n"
        "How might we provide instant unit context?\n",
        encoding="utf-8",
    )

    (canonical / "personas" / "part-time-landlord.md").write_text(
        "---\n"
        'title: "Part Time Landlord"\n'
        "---\n\n"
        "### Description\n\n"
        "Owns 1-3 units and self-manages.\n\n"
        "### User Goal\n\n"
        "Resolve tenant requests quickly.\n\n"
        "### User Needs\n\n"
        "Fast context retrieval across docs and contacts.\n\n"
        "### User Mindset\n\n"
        "I am not a professional property manager.\n",
        encoding="utf-8",
    )

    cards = module.collect_cards(canonical)
    output_dir = tmp_path / "render" / "content"
    module.write_outputs(cards, output_dir)

    vision = (output_dir / "slide-001" / "content.yaml").read_text(encoding="utf-8")
    scenario = (output_dir / "slide-002" / "content.yaml").read_text(encoding="utf-8")
    persona = (output_dir / "slide-003" / "content.yaml").read_text(encoding="utf-8")

    assert "A clear operational back office." in vision
    assert "Context reconstruction is expensive." in vision
    assert "Description" in scenario
    assert "Tenant reports sink issue at 9pm." in scenario
    assert "Scenario Narrative" in scenario
    assert "Landlord searches across tools before responding." in scenario
    assert "How Might We" in scenario
    assert "How might we provide instant unit context?" in scenario
    assert "Description" in persona
    assert "Owns 1-3 units and self-manages." in persona
    assert "Goal" in persona
    assert "Resolve tenant requests quickly." in persona
    assert "Needs" in persona
    assert "Fast context retrieval across docs and contacts." in persona
    assert "Mindset" in persona
    assert "I am not a professional property manager." in persona


def test_use_case_slide2_replaces_primary_user_placeholder(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    (canonical / "use-cases").mkdir(parents=True)

    (canonical / "use-cases" / "resolve-maintenance.md").write_text(
        "---\n"
        'title: "Resolve Maintenance"\n'
        "---\n\n"
        "### Use Case Description\nA\n\n"
        "### Use Case Overview\nB\n\n"
        "### Business Value\nC\n\n"
        "### Primary User\nIncoming Shift Supervisor\n\n"
        "### Secondary User\nMaintenance Technician\n\n"
        "### Preconditions\nIssue exists\n\n"
        "### Steps\n1. Review issue\n\n"
        "### Data Requirements\nAsset id\n\n"
        "### Equipment Requirements\nDevice\n\n"
        "### Operating Environment\nFloor\n\n"
        "### Success Criteria\nResolved\n\n"
        "### Pain Points\nDelay\n\n"
        "### Extensions\nEscalate\n\n"
        "### Evidence\nInterview\n",
        encoding="utf-8",
    )

    cards = module.collect_cards(canonical)
    output_dir = tmp_path / "render" / "content"
    module.write_outputs(cards, output_dir)

    # Use Case expands to 4 slides; slide 2 should include primary user replacement.
    rendered = (output_dir / "slide-002" / "content.yaml").read_text(encoding="utf-8")
    assert "{{UC_PRIMARY_USER}}" not in rendered
    assert "Incoming Shift Supervisor" in rendered


def test_configure_logging_does_not_raise() -> None:
    module = _load_module()
    module.configure_logging(False)
    module.configure_logging(True)


def test_parse_frontmatter_no_marker_returns_empty_fields() -> None:
    module = _load_module()
    fields, body = module.parse_frontmatter("plain text without frontmatter")
    assert fields == {}
    assert body == "plain text without frontmatter"


def test_extract_first_heading_no_match_returns_empty() -> None:
    module = _load_module()
    assert module.extract_first_heading("no headings here") == ""


def test_extract_intro_block_no_match_returns_empty() -> None:
    module = _load_module()
    assert module.extract_intro_block("no intro block") == ""


def test_infer_artifact_type_unknown(tmp_path: Path) -> None:
    module = _load_module()
    path = tmp_path / "unrecognized.md"
    path.write_text("content", encoding="utf-8")
    assert module.infer_artifact_type(path) == "Unknown"


def test_template_for_type_unsupported_raises() -> None:
    import pytest

    module = _load_module()
    with pytest.raises(ValueError, match="Unsupported artifact type"):
        module.template_for_type("Nonexistent Type")


def test_scenario_summary_with_all_sections() -> None:
    module = _load_module()
    body = (
        "### Description\nA description.\n\n"
        "### Scenario Narrative\nA narrative.\n\n"
        "### How Might We\nA question.\n"
    )
    result = module._scenario_summary(body)
    assert "Description" in result
    assert "Scenario Narrative" in result
    assert "How Might We" in result


def test_vision_sections_extracts_fields() -> None:
    module = _load_module()
    body = "## Vision Statement\nBig vision.\n\n### Why This Matters\nContext.\n"
    sections = module._vision_sections(body)
    assert "V_VISION_STATEMENT" in sections
    assert "V_WHY_THIS_MATTERS" in sections
    assert "Big vision." in sections["V_VISION_STATEMENT"]


def test_problem_sections_primary_field() -> None:
    module = _load_module()
    body = "## Problem Statement\nThe core problem.\n"
    sections = module._problem_sections(body)
    assert "P_PROBLEM_STATEMENT" in sections
    assert "The core problem." in sections["P_PROBLEM_STATEMENT"]


def test_problem_sections_fallback_customer_friendly() -> None:
    module = _load_module()
    body = "## Customer-friendly summary\nUsers need help.\n"
    sections = module._problem_sections(body)
    assert "P_PROBLEM_STATEMENT" in sections
    assert "Users need help." in sections["P_PROBLEM_STATEMENT"]


def test_parse_card_unknown_type_returns_none(tmp_path: Path) -> None:
    module = _load_module()
    file = tmp_path / "unknown-file.md"
    file.write_text("# Some Title\nContent here.", encoding="utf-8")
    result = module.parse_card(file, tmp_path)
    assert result is None


def test_render_slide_with_sections_populates_placeholders() -> None:
    module = _load_module()
    card = module.Card(
        artifact_type="Vision Statement",
        title="Test Vision",
        summary="",
        source_path="vision-statement.md",
        last_updated="2026-04-22",
        sections={
            "V_VISION_STATEMENT": "The vision text.",
            "V_WHY_THIS_MATTERS": "It matters.",
        },
    )
    rendered = module.render_slide(card, 1)
    assert "The vision text." in rendered
    assert "It matters." in rendered


def test_create_parser_returns_parser() -> None:
    module = _load_module()
    parser = module.create_parser()
    assert parser is not None


def test_main_missing_canonical_dir_returns_1(tmp_path: Path) -> None:
    module = _load_module()
    missing = tmp_path / "nonexistent"
    with unittest.mock.patch.object(
        sys, "argv", ["prog", "--canonical-dir", str(missing)]
    ):
        result = module.main()
    assert result == 1


def test_main_no_recognized_cards_returns_1(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    canonical.mkdir()
    (canonical / "README.md").write_text("# Readme\n", encoding="utf-8")
    output_dir = tmp_path / "output"
    with unittest.mock.patch.object(
        sys,
        "argv",
        ["prog", "--canonical-dir", str(canonical), "--output-dir", str(output_dir)],
    ):
        result = module.main()
    assert result == 1


def test_main_success_returns_0(tmp_path: Path) -> None:
    module = _load_module()
    canonical = tmp_path / "canonical"
    canonical.mkdir()
    (canonical / "vision-statement.md").write_text(
        "---\ntitle: Great Vision\n---\n\n"
        "## Vision Statement\nA clear future state.\n\n"
        "### Why This Matters\nContext matters.\n",
        encoding="utf-8",
    )
    output_dir = tmp_path / "output"
    with unittest.mock.patch.object(
        sys,
        "argv",
        ["prog", "--canonical-dir", str(canonical), "--output-dir", str(output_dir)],
    ):
        result = module.main()
    assert result == 0
    assert (output_dir / "slide-001" / "content.yaml").exists()

Requirements and compatibility

Written for: GitHub Copilot

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: Needs a command-line tool, Needs Python

The author says: Requires Python 3.11+ and uv

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/experimental/customer-card-render · 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 customer-card-render.zip