Chart async message topology from committed C# publish, send, and consumer registrations. Orphan publishers and consumers are findings. Same short name in two namespaces is two messages. Use when: 'map events', 'who publishes this message', 'message topology', 'orphan consumer', 'publish subscribe'. Skip when: the question is an in-process call trace (map-flow) or broker hosts and replicas (a deployment view).
Pro scans all 5 files and shows the line behind each finding
Scanned 10/4/2026
npx -y skills add melodic-software/claude-code-plugins --skill map-events --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Map Events?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/melodic-software-map-events)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
description: "Chart async message topology from committed C# publish, send, and consumer registrations. Orphan publishers and consumers are findings. Same short name in two namespaces is two messages. Use when: 'map events', 'who publishes this message', 'message topology', 'orphan consumer', 'publish subscribe'. Skip when: the question is an in-process call trace (map-flow) or broker hosts and replicas (a deployment view)."
argument-hint: "[--unrouted-only] [--out <dir>]"
user-invocable: true
disable-model-invocation: false
shell: bash
metadata:
workflow-stage: explore
summary: Chart publishers, consumers, and orphan messages
---
## Repository context
The current repository is both the CONSUMER and the DEFAULT SUBJECT. Collect the project root with
one Bash call, `git rev-parse --show-toplevel`. A failure is an unknown value; `${CLAUDE_PROJECT_DIR}`
is the root either way.
## Purpose
Answer "who publishes which message, and who consumes it" from committed C# call sites and handler
registrations. Every edge cites a file and a line. The scripts collect and render. Do not add an
edge the script did not emit, and do not drop a publish whose contract type did not resolve.
The shipped adapter is the MassTransit shape: `Publish<T>` or `Publish(new T(..))` is broadcast,
`Send<T>` or `Send(new T(..))` is point-to-point, `IConsumer<T>` consumes `T`. `AddConsumer<C>` and
`ConfigureConsumer<C>` register the consumer class `C`; they are not messages. Inside a
`ReceiveEndpoint("q", ..)` call, on its own line or in its block, they put `C`'s consume edges on
queue `q`; a registration outside it and every publish or send edge has no queue. A message is a type some
publish, send, or consume names; a service or consumer class is not one. Identity is the
namespace-qualified type. In-process calls are `/architecture:map-flow`. A cross-process hop in
that trace is a hand-off to this skill.
Read: C# (MassTransit shape). Declined: Node, Go, Python, Rust, JVM, Ruby, and PHP. Their files are
never read, so a message only they publish or consume is absent from the chart, not a finding.
## Resolve home
Read `${CLAUDE_PLUGIN_ROOT}/reference/config.md` first. This skill writes into `architecture_dir`.
It reads no dialect key and adds none: it always writes `events.md`, a mermaid flowchart (dotted
broadcast, solid point-to-point). The authoring-formats convention keys a dialect only for data
diagrams and C4 system views. A message-topology flowchart is neither, so it follows the unkeyed
mermaid rule, as `/planning:design`'s sequence flows do.
Run `bash "${CLAUDE_PLUGIN_ROOT}/lib/resolve-convention-home.sh" --root "${CLAUDE_PROJECT_DIR}"`.
Exit 0 means read `<home>/architecture/README.md` for `architecture_dir`. Exit 1, 2, and 3 mean
there is no declared home.
`--out <dir>` wins for this run. Then a declared `architecture_dir`. Then one question.
`architecture_dir` has NO default. An undeclared and unconfirmed home, including every
non-interactive run, STOPS and points at `/architecture:setup`.
## Build the record
```bash
"${CLAUDE_SKILL_DIR}/scripts/collect-events.sh" \
--repo "<subject-repo>" --out "<architecture_dir>/events.json"
```
The record is schema_version 1. Message lines start with `{"id":`. Edge lines start with
`{"from":`. Finding lines start with `{"kind":`. A message line cites the file and line that
declare the type. An edge's `from` is the file of the publishing, sending, or consuming call site,
`to` is the resolved message (`-` when unresolved), and `kind` is `publish`, `send`, or `consume`. Findings include orphan publishers, orphan consumers, fan-out past
`fanout_threshold` (3), competing consumers on one queue, and unresolved edges.
`fanout_threshold` is this plugin's limit, not the broker's.
A short type name resolves through the file's namespace, its parent namespaces, and its `using`
directives, then through the one repo type with that name. A dotted name is tried as written, then
under each enclosing namespace and each `using`; `global::` allows only the as-written match. A
`Publish(` or `Send(` whose type does not resolve (a variable, an anonymous or target-typed `new`,
an undeclared or ambiguous name, a dotted name that matches no declared type) is an edge with
`resolution: unresolved` and an `unresolved` finding naming the kind, the type text or `-`, and
`file:line`. It is never omitted, and an unresolved consumer is never an orphan.
## Render
```bash
"${CLAUDE_SKILL_DIR}/scripts/render-events.sh" \
--record "<architecture_dir>/events.json" --out "<architecture_dir>"
```
Add `--unrouted-only` when the invocation asked for orphans and unresolved edges only. The findings
list is still written.
The script prints:
`events: messages=<n> publishers=<n> consumers=<n> unresolved=<n> orphans=<n> unrouted_only=<yes|no>`.
The artifact's Handoff section lists one line per publish or send edge, keyed by `file:line` so it
matches a `/architecture:map-flow` hop cite; map-flow reads nothing from `events.md`:
`handoff: map-events contract=<type> direction=<publish|send> file=<path> line=<n>`.
Exit 1 means the record is unreadable or not one object per line. Nothing was written.
## Close with the report
- **Artifacts**: each path written, or `none written` when the run stopped before a home existed.
- **Messages**: `messages=` from the summary. Say that identity is the resolved type.
- **Edges**: `publishers=`, `consumers=`, `unresolved=`.
- **Findings**: `orphans=` and the other finding kinds present.
- **Filter**: `unrouted_only=` yes or no. Findings are listed either way.
- **Dialect**: mermaid flowchart. `landscape_dialect` was not read. No key was added.
- **Handoff**: that cross-process edges are in the Handoff section, keyed by `file:line` to match a map-flow hop cite.
## Interactive view
After the report, offer an interactive view of `events.json` in one sentence. The markdown and the record
stay authoritative. Build it only with `${CLAUDE_PLUGIN_ROOT}/scripts/build-view.mjs events`, never
hand-written; the publish destination comes from the `medium` cascade key. Procedure:
[`${CLAUDE_PLUGIN_ROOT}/reference/rendered-view.md`](${CLAUDE_PLUGIN_ROOT}/reference/rendered-view.md).
## What this skill does NOT do
- In-process synchronous calls. Those are `/architecture:map-flow`.
- Retry, dead-letter, or delivery guarantees.
- Broker hosts, replicas, or partitions. Those are a deployment view.
- Treat two types with the same short name as one message.
- Drop an unresolved publish.
- Read `landscape_dialect` or add a dialect key. The flowchart is always mermaid.
- Invent a home.
## Next
- Trace one entry point up to the broker: `/architecture:map-flow <entry>`.
- The question is which deployables bind the broker: `/architecture:map-containers`.
- The view settles a decision worth keeping: `/architecture:record-decision`.
## Gotchas
- **Publish and send are different.** MassTransit `Publish<T>` is broadcast. `Send<T>` is
point-to-point. `IConsumer<T>` is a consumer of `T`. Verified 2026-09-28 against
<https://masstransit.io/documentation/concepts/consumers>. Recheck when that page stops using
`IConsumer<T>` as the consumer contract, or when the producers page stops distinguishing
publish from send.
- **Identity is the resolved type.** `Billing.Contracts.OrderPlaced` and
`Shipping.Contracts.OrderPlaced` are different messages. A short name that matches two types
stays unresolved rather than being joined, and a dotted name that matches no declared type
(an external package's contract, for one) stays unresolved rather than being trusted.
- **An unresolved publish or send is a finding.** Dropping it would make an orphan-consumer
finding wrong. The edge is listed with `resolution: unresolved` and satisfies no consumer.
- **A registered consumer is not a message.** `AddConsumer<C>` names the consumer class. The
message is the `T` in `C`'s `IConsumer<T>`, so a registration never yields an orphan.
- **Findings are not the diagram.** `--unrouted-only` filters arrows. The findings list remains.
- **No dialect key.** The flowchart is mermaid whatever `landscape_dialect` says: a dotted arrow
for broadcast, a solid arrow for point-to-point.
- **A reformatted record is refused.** `render-events.sh` exits 1 and writes nothing.
- **The scan is the MassTransit shape, one line at a time.** `Publish<T>`, `Send<T>`,
`.Publish(new T`, `.Send(new T`, `IConsumer<T>`, `AddConsumer<C>`, and `ConfigureConsumer<C>`.
A call split across lines is not joined. A `.Publish(` or `.Send(` from another library is
still listed, not dropped.
- **Only `ReceiveEndpoint("literal", ..)` binds a queue.** A topic, exchange, subject, or
configuration-file binding is not read, and neither is a queue name held in a variable. A
consumer bound that way shows no queue, so no competing-consumer finding is raised for it.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!