| name | feature-diagram |
| description | Per-feature Mermaid flowchart — routes / models / jobs / screens. Consumes /extract-features inventory. |
| argument-hint | <feature-slug> [project-name] [--force] |
| allowed-tools | Bash, Read, Grep, Glob, Write, Edit |
/feature-diagram — Per-feature Mermaid Sub-graph
Reads the Feature Inventory at projects/<name>/feature-inventory.md (produced by /extract-features) and writes a per-feature Mermaid flowchart LR showing the HTTP routes, data models, async jobs, and UI screens that participate in one feature. Output: projects/<name>/features/<slug>.md (one file per feature).
This skill closes the gap left by the rest of the architecture-doc family:
| Skill | Slice |
|---|
/c4 | Whole-system topology (L1 + L2) |
/dfd | Whole-system data flows + trust boundaries |
/process | One business process (BPMN control flow) |
/journey | One feature, product-facing user flow (HTML modal-per-page) |
/feature-diagram (this skill) | One feature, architectural slice (Mermaid flowchart) |
See AgDR-0035-per-feature-diagrams.md for the design rationale.
Path resolution
Read the per-project docs dir via portfolio_projects_dir from .claude/hooks/_lib-portfolio-paths.sh. Source the helper at the top of any bash block that touches that path:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
Defaults match today's single-fork layout (./projects). Adopters in split-portfolio mode override the portfolio.{registry, projects_dir} keys in .claude/project-config.json — the helper resolves whichever mode they're in. See docs/multi-project.md.
Write targets (see me2resh/apexyard#373 + #443): paths documented as projects/<name>/X in this skill are canonical adopter-facing forms — implement them in bash as "${projects_dir}/<name>/X". Never construct from "${PWD}/projects/...", "$(git rev-parse --show-toplevel)/projects/...", or a literal ./projects/... — those break in split-portfolio v2 mode where projects_dir resolves to a sibling repo.
REQUIRED per-block preamble (see #443): Claude executes each bash block as a separate shell invocation. The projects_dir assignment from the Path resolution section above does NOT carry into later blocks. Every bash block that writes to a projects/<name>/X path MUST start with this three-line preamble so it's self-contained:
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
The Path resolution section's example sources the helper once for documentation purposes; it does not absolve later blocks from sourcing it themselves. Treat each bash fence as a fresh process.
Usage
/feature-diagram create-order # current project (cwd inside workspace/<name>/ or single-project fork)
/feature-diagram create-order curios-dog # registered project
/feature-diagram reset-password --force # overwrite existing diagram
If <project-name> is omitted and cwd is the ops-fork root with multiple registered projects, the skill asks which project's inventory to read.
Output location
projects/<name>/features/<slug>.md ← the per-feature diagram (one file per feature)
projects/<name>/feature-inventory.md ← updated to link the Feature column to the new file
The features/ directory is created if missing.
Activated role
When /feature-diagram runs, activate the Tech Lead role — they own architectural slicing and the "which surfaces participate in this feature" view. See .claude/rules/role-triggers.md for the full activation protocol.
Process
1. Resolve the target
- If
<project-name> is given and the registry has it → use that project.
- If no
<project-name> and cwd is inside workspace/<name>/ → use that project.
- If no
<project-name> and cwd is the ops fork root → ask which registered project (list from apexyard.projects.yaml).
- If exactly one project is registered → use it without asking.
2. Locate the inventory
The inventory file must exist at <projects_dir>/<name>/feature-inventory.md. If missing, stop and tell the operator:
No feature inventory at projects/<name>/feature-inventory.md.
Run /extract-features <project> first — that produces the inventory
this skill reads from.
3. Resolve the feature slug
The slug matches a row in the inventory's consolidated feature matrix. Match in this order:
- Exact slug match in an existing
[link text](features/<slug>.md) cell — re-runs.
- Feature title match (kebab-case the title and compare) — e.g. operator passes
create-order and a row says Create order.
- Substring match — e.g.
password matches Reset password via email. Ambiguous matches (≥ 2 rows) ask the operator to disambiguate.
If no match → exit 2 with a helpful message listing the available slugs:
No feature matches slug 'creat-order' in projects/<name>/feature-inventory.md.
Available slugs:
create-order
reset-password-via-email
bulk-delete-users
...
Re-run with one of these, or check the inventory file.
4. Extract the row's surfaces
For the matched row, parse the inventory's per-axis findings tables (HTTP routes, Data models, Async jobs, UI screens) and collect every element whose Notes / File / Source column ties back to this feature.
The inventory's row has a Source column that already names the axes that corroborated the feature (e.g. route + test + UI, model only). Use that as the primary filter:
Source says | Subgraphs populated |
|---|
route + test + UI + job | All four (Routes, Screens, Jobs; Models if any route writes to one) |
route + UI | Routes + Screens |
model only | Models (plus a coverage-gap note) |
doc only | Empty diagram with a coverage-gap note pointing at the README/CHANGELOG entry |
When the inventory row references specific handlers / models / jobs / components by file path (the per-axis tables in the inventory are keyed by File), follow those references and include each as a node in the matching subgraph.
5. Generate the Mermaid
Build a flowchart LR with four named subgraphs:
flowchart LR
subgraph Screens["UI Screens"]
screen_<id>["<Display name><br/>(<file>)"]
...
end
subgraph Routes["HTTP Routes"]
route_<id>["<METHOD> <path><br/>(<file>)"]
...
end
subgraph Models["Data Models"]
model_<id>["<Name><br/>(<file>)"]
...
end
subgraph Jobs["Async Jobs"]
job_<id>["<Name><br/>(<file>)"]
...
end
%% Edges inferred from the inventory row
screen_<id> -->|submits| route_<id>
route_<id> -->|reads/writes| model_<id>
route_<id> -->|enqueues| job_<id>
job_<id> -->|reads/writes| model_<id>
Empty subgraphs stay visible — render <Subgraph>_empty["(none)"] as a placeholder so the four-quadrant shape is constant across features:
subgraph Jobs["Async Jobs"]
Jobs_empty["(none)"]
end
Node IDs are stable: screen_<slug>, route_<slug>, model_<slug>, job_<slug> where <slug> is a kebab-case-ified version of the element's name. Stability matters because operators may hand-add a comment or annotation under a specific node ID; re-runs should keep those grounded.
Edge inference rules — same as documented in AgDR-0035:
| Source axis | Target axis | Edge label |
|---|
| Screen | Route | "submits" if the screen has form fields; "calls" otherwise |
| Route | Model | "reads/writes" |
| Route | Job | "enqueues" |
| Job | Model | "reads/writes" (only if the inventory row mentions both as participating) |
Don't invent edges. If the inventory row doesn't corroborate a screen → route mapping (no shared URL prefix, no shared component name), don't draw the edge — emit the nodes only and add a one-line coverage-gap note at the bottom of the file.
6. Write the file
Path: <projects_dir>/<name>/features/<slug>.md. Output template:
# <Feature title>
> Per-feature architectural slice for **<Feature title>**. Generated from the consolidated feature matrix in [`../feature-inventory.md`](../feature-inventory.md).
**Status**: <Active | Deprecated | Untested | Documented but not in code>
**Source axes**: <route + test + UI + job>
**Notes**: <Notes column from the inventory row>
## Diagram
```mermaid
flowchart LR
...
```
## Participating elements
### HTTP Routes (<N>)
| Method | Path | Handler | File |
|--------|------|---------|------|
| ... | ... | ... | ... |
### Data Models (<N>)
| Model | Fields | File |
|-------|--------|------|
| ... | ... | ... |
### Async Jobs (<N>)
| Job | Trigger | Handler | File |
|-----|---------|---------|------|
| ... | ... | ... | ... |
### UI Screens (<N>)
| Route | Component | Fields | File |
|-------|-----------|--------|------|
| ... | ... | ... | ... |
## Coverage gaps
- <Any edges the inventory didn't corroborate; any axes that came up empty>
---
_Generated by `/feature-diagram` on YYYY-MM-DD. Re-run when the feature's surfaces change._
Behaviour on existing file:
- File doesn't exist → write directly.
- File exists, no
--force → prompt the operator with a default-no offer to overwrite. Print a diff against the proposed content if possible.
- File exists,
--force → overwrite without prompting; print a one-line Overwriting <path> to stderr.
7. Update the inventory back-link
In <projects_dir>/<name>/feature-inventory.md's consolidated feature matrix table, replace the matched row's Feature cell with a markdown link:
| 1 | [Create order](features/create-order.md) | API + UI | Active | route + test + UI | POST /api/orders; charges Stripe; sends confirmation email |
Idempotent — if the cell already contains a link to the same target file, leave it. If the cell contains a link to a different target file (rare; manual rename), overwrite.
If the inventory is read-only (no write permission, or the operator passed --no-update-inventory) the back-link step is skipped with a one-line warning.
8. Lint the generated Mermaid
Run lint.sh against the output file:
SKILL_DIR="$(dirname "$(realpath "$0")")"
"$SKILL_DIR/lint.sh" "$out_file" || lint_rc=$?
The lint wraps the shared _lib-mermaid-lint.sh — extracts the ```mermaid flowchart block and validates each via mmdc (mermaid-cli). Graceful-degrades when Node / npx is unavailable (exit 3, advisory only).
Treat exit 1 (parse error) as a hard fail — print the lint output, ask the operator whether to (a) auto-regenerate the offending block, (b) keep the file as-is and fix by hand, or (c) re-run with --skip-lint if mmdc is misbehaving. Exit 3 (Node missing) prints a one-line warning and proceeds.
9. Confirm to the user
✓ <project>: per-feature diagram written
Diagram: <projects_dir>/<name>/features/<slug>.md
Inventory back-link: updated row <N> in feature-inventory.md
Routes: 3 Models: 2 Jobs: 1 Screens: 1
Mermaid lint: 1 of 1 block parsed cleanly
Preview: open the file on GitHub — Mermaid renders inline.
Re-run /feature-diagram <slug> --force when the feature's surfaces change.
Rules
- Read-only against the codebase. Never modify the project's source. Only writes to
projects/<name>/features/<slug>.md and updates the inventory's back-link.
- The inventory is a hard dependency. If
feature-inventory.md doesn't exist, refuse with a pointer to /extract-features. Don't auto-run it (that's a heavyweight scan; the operator should know they're triggering it).
- Don't auto-overwrite. Existing diagrams require explicit
--force. The diagrams may have been hand-edited.
- Empty subgraphs stay visible. Four quadrants every time; render
(none) placeholders so the reader's eye doesn't have to re-orient between features.
- Don't invent edges. Every edge must trace to a column in the inventory row. Missing corroboration becomes a coverage-gap note, not a fabricated arrow.
- Stable node IDs.
screen_<slug> / route_<slug> / model_<slug> / job_<slug> — slug is kebab-case-ified element name. Reproducibility matters; operators may annotate by ID.
- Footer signature is mandatory. Every generated file ends with the
Generated by /feature-diagram on YYYY-MM-DD line so future readers know it's regenerable.
- Refuse if the slug doesn't match a row. Exit 2 with the list of available slugs — no silent best-guess fallback.
When to use this
| Trigger | Use /feature-diagram? |
|---|
| Onboarding a new engineer to a specific feature | Yes — pairs with /handover and /extract-features |
| Planning a refactor scoped to one feature | Yes — visualises the blast radius before code edits |
| Greenfield rewrite, slicing the v1 scope by feature | Yes — each "must-have v1" feature gets a per-feature diagram showing what must be preserved |
| Major arch change to one feature (new model added, new job triggered) | Yes — re-run with --force |
| Whole-system view | No — use /c4 (topology) or /dfd (data flows) |
| User-flow preview for stakeholders | No — use /journey (product-facing HTML) |
Out of scope (v1)
- Cross-feature diagrams (showing how features depend on each other) — listed as out-of-scope in #288.
- Per-feature sequence diagrams — use
templates/architecture/sequence.md manually if needed.
- Trust-boundary overlays — that's
/dfd's slice. If the operator wants security-scoped feature analysis, run /dfd and /feature-diagram side by side.
- Auto-running
/extract-features — the inventory is a hard dependency, not an optional auto-trigger.
- Cross-project per-feature diagrams — single project per invocation. Multi-project per-feature is a separate concern.
Anti-patterns
- Don't substitute
/feature-diagram for /c4. The per-feature slice shows ONE feature's surfaces. System-wide topology lives in /c4.
- Don't run
/feature-diagram for every row on every PR. It's refresh-on-arch-change, not per-PR. The whole inventory only changes when features ship; the per-feature diagrams only change when the feature's routes / models / jobs / screens change.
- Don't hand-edit a generated diagram and lose the footer. Re-runs detect the footer; without it the skill can't tell if a file is regenerable. If you must hand-edit, keep the footer line in place.
- Don't treat coverage gaps as failures. The inventory has gaps by design (the scanner can't see business rules, permission matrices, or implicit features). Per-feature diagrams inherit those gaps — that's a feature, not a bug; the gaps are surfaced explicitly so a human can fill them in.
Part of ApexYard — multi-project SDLC framework for Claude Code · MIT.