| description | This skill visualizes the domain object hierarchy in the project using domainchart npm package. |
| metadata | {"github-path":"skills/domainchart","github-ref":"refs/heads/main","github-repo":"https://github.com/kt3k/domainchart","github-tree-sha":"ca4b540ea091684b84b444bbacd87e18518b272d"} |
| name | domainchart |
domainchart
A skill for expressing a DDD domain as a hierarchy of domain objects that
can be rendered as a tree-style diagram. The focus is on modeling the structure
correctly; the renderer is just a thin step at the end.
Scope (argument)
The skill takes an optional argument that names the scope to analyze — file
paths, directory paths, or a free-form description (e.g. "the billing module").
When no argument is given, treat the entire codebase as the scope.
Workflow
Execute the steps below in order. Do not skip ahead.
- Read the code in scope. Carefully read every file under the given scope
(or the whole repository if no argument was provided). Note classes, types,
structs, schemas, and any data structures that look like domain concepts.
- List the domain objects. Enumerate each candidate as either an Entity
(identity-bearing, independent lifecycle — 🔷) or a Value Object (defined
purely by its attributes, interchangeable when equal — 💎). IDs like
OrderId / UserId are Value Objects. One node per concept; don't merge.
- Read
npx domainchart types. Run the command and read its JSON Schema
output. This is the authoritative input shape — do not guess the schema from
memory.
- Map the listed objects to that schema. Produce a JSON document that
conforms to the schema from step 3:
- Use a child's
name as the parent property's type to express the
parent→child edge. Collections / optionals / unions all count as
references: OrderItem[], Array<OrderItem>, Set<OrderItem>,
OrderItem?, Foo | Bar, Array<Foo | Bar>.
- Any
type that does not match a declared name is a primitive leaf
(string, number, Date, …).
- Pick aggregate roots intentionally — a root owns its children, and children
should be reachable only through their root. Break cross-aggregate cycles
by referencing the other root's ID, not the root itself.
- Save the JSON to a temp directory. Create one with
mktemp -d and write
the document to <tmp>/domainchart.json. Do not write to the project yet.
- Validate. Run
npx domainchart validate <tmp>/domainchart.json. If any
errors or warnings are reported, fix the JSON and re-run. Repeat until the
output is clean.
- Build the HTML. Run
npx domainchart build <tmp>/domainchart.json -o <tmp>/domainchart.html.
- Open it in a browser so the user can see the result
(
open <tmp>/domainchart.html on macOS).
- Ask whether to keep it. Ask the user whether to copy
domainchart.json
and domainchart.html into the project, and where. Only copy if they
confirm.
Hierarchy design guidelines
- One root per aggregate. If two candidates both want to own the same child,
decide which truly owns it; the other should reference it by ID (a Value
Object like
OrderId) instead of by object.
- Break cross-aggregate cycles with ID references. A direct object reference
to
OtherAggregateRoot creates a child edge; referencing its ID
(OtherAggregateRootId as a Value Object) keeps the two aggregates separate.
- Cycles with no clear root are rejected. If the validator complains, the
hierarchy is ambiguous — pick a root or break the cycle via an ID.
- Depth follows ownership, not data flow. Put a property under its owner,
not under whoever happens to read it.
Example
{
"title": "EC Site Domain Model",
"models": [
{
"name": "Order",
"kind": "entity",
"description": "Order aggregate root",
"properties": [
{ "name": "id", "type": "OrderId" },
{ "name": "items", "type": "OrderItem[]" },
{ "name": "customerId", "type": "CustomerId" }
]
},
{
"name": "OrderItem",
"kind": "entity",
"properties": [
{ "name": "quantity", "type": "number" },
{ "name": "unitPrice", "type": "Money" }
]
},
{
"name": "Money",
"kind": "value_object",
"properties": [
{ "name": "amount", "type": "number" },
{ "name": "currency", "type": "string" }
]
},
{
"name": "OrderId",
"kind": "value_object",
"properties": [{ "name": "value", "type": "string" }]
},
{
"name": "CustomerId",
"kind": "value_object",
"properties": [{ "name": "value", "type": "string" }]
}
]
}
Inferred result: Order becomes the aggregate root, owning OrderItem (which
owns Money) and OrderId. CustomerId is a leaf — the Customer aggregate
is referenced only by ID, keeping the two aggregates separate.