protean docs
The protean docs command group has two subcommands:
protean docs generaterenders architecture documentation from a domain or an IR JSON file.protean docs previewstarts a live preview server for Protean documentation.
protean docs generate
Generate architecture diagrams and catalogs from a domain's Intermediate Representation.
# From a live domain
protean docs generate --domain=my_app --type=events
# From an IR JSON file
protean docs generate --ir=domain-ir.json --type=event-model
Options
--domain,-d: Path to the domain module. Mutually exclusive with--ir.--ir: Path to an IR JSON file. Mutually exclusive with--domain.--type,-t: What to generate. One of:clusters: aggregate cluster class diagrams.events: command-to-event flow diagrams plus downstream consumers.handlers: handler wiring diagrams.catalog: an event and command catalog (Markdown tables).event-model: the EventModeling slice timeline (see below).llms: a versionedllms.txtcontext pack (see below).agents: a versionedAGENTS.mdnegative-constraint pack (see below).all(default): every section exceptevent-model,llms, andagents.
--format,-f:markdown(fenced code blocks, the default) ormermaid(raw diagram source).mermaidis not supported forcatalog,llms, oragents.--output,-o: Write to a file instead of stdout.--cluster: Filter to a single cluster FQN (only with--type=clustersor--type=all).--annotations: Path to an annotations TOML file (only with--type=event-model). Defaults to.protean/annotations.tomlwhen present. See Annotating the event model.--check: Only with--type=llms. Compare a committed snapshot against a fresh render, write nothing, and exit non-zero on drift. See Checking a committed snapshot for drift.
The event model slice timeline
--type=event-model renders the domain as an
EventModeling slice timeline. Each aggregate
cluster becomes one slice that reads left to right:
- Command(s) that trigger the aggregate.
- Aggregate (state) that decides and holds state.
- Event(s) the aggregate raises (fact events are omitted).
- Read models and automations that consume those events. Projectors are
read models, drawn as cylinders. Event handlers and process managers are
automations, drawn as hexagons, so a read model and an automation are told
apart at a glance. A process manager also shows its
start/endlifecycle, both in its node label and on the edge from the event.
Consumers are matched to a slice's events by the event type, so the whole diagram is derived from the IR alone.
In Markdown output each slice leads with a structural Given-When-Then before the diagram:
- Given the aggregate the slice is about.
- When the cluster's commands (the triggers).
- Then the cluster's non-fact events (the results).
The GWT is derived structurally from the IR. It is slice-level: a slice with
several commands or events lists them all on the When and Then lines rather
than pairing each command with the event it produces. Commands-only or
events-only slices drop the line they have nothing for. This GWT is a
Markdown-only lead; the --format=mermaid output stays a bare flowchart.
# One combined flowchart of all slices
protean docs generate --domain=my_app --type=event-model --format=mermaid
# One GWT-led diagram per slice, as Markdown
protean docs generate --domain=my_app --type=event-model
Unlike the other diagram types, event-model is its own view and is not
included in --type=all.
The llms.txt context pack
--type=llms renders a versioned context pack in the
llms.txt convention, aimed at an LLM agent working on a
Protean project. It has two layers:
- Framework layer (always present): an H1 naming Protean and the installed version, a one-line summary, and a "Core documentation" section of links to the published docs site for the core areas: aggregate clusters, events, command and event handlers, projections, and event evolution.
- Project overlay (present only with a source): a section derived from the IR listing the project's own aggregate clusters (with their commands, events, and handlers) and its projections.
--type=llms is the one type that runs with no --domain and no --ir: with
no source it emits the framework layer alone; with a source it adds the project
overlay. Like event-model, it is its own view and is not part of --type=all.
The output is byte-stable: for a given Protean version and project structure, generating twice yields identical bytes. The overlay reads only structural parts of the IR, never volatile fields like the build timestamp, so it does not churn between runs.
# Framework layer only (no source needed)
protean docs generate --type=llms
# Framework layer plus the project overlay
protean docs generate --domain=my_app --type=llms --output=llms.txt
Checking a committed snapshot for drift
If you commit the llms.txt snapshot to the repository, the file drifts as the
domain changes. --check catches that drift. It renders the snapshot fresh in
memory, compares it byte for byte to the committed file, writes nothing, and
exits non-zero when they differ. A missing file counts as drift. This is the
black --check idiom, and the exit codes match protean dx check: 0 when the
file is up to date, 1 on drift.
# Fails (exit 1) if llms.txt is out of date; writes nothing
protean docs generate --type=llms --domain=my_app --output=llms.txt --check
The snapshot is a secondary path. The protean mcp
server is the recommended way to give an agent domain knowledge: it answers
live from the installed domain, so there is no file to keep fresh. Reach for a
committed snapshot only for agents that do not speak MCP, and know the
trade-off: a file can go stale, and --check is what tells you it has. See
Giving an agent domain context.
The opt-in domain_snapshot config key
Repeating --domain and --output on every generate and --check run is
noise. The [tool.protean.docs].domain_snapshot key names both once. It is
absent by default: set it only when you commit a snapshot. Nothing reads it
until you add it, and protean new and protean dx never write it.
In pyproject.toml:
[tool.protean.docs]
domain_snapshot = { path = "llms.txt", domain = "src/my_app/domain.py" }
In a domain.toml the same key sits under [docs] (a domain.toml maps
directly to the Protean namespace, with no tool.protean prefix):
[docs]
domain_snapshot = { path = "llms.txt", domain = "src/my_app/domain.py" }
domain takes the same value --domain takes: a file, a package, or a dotted
import path, with an optional :instance suffix. A project made by
protean new keeps the domain in src/<package>/domain.py and leaves the
package's __init__.py empty, so name the file. domain = "my_app" there
resolves to the package, which holds no domain, and the command stops with a
load error.
With the key set, both commands run with no repeated flags:
# Writes the snapshot to the configured path from the configured domain
protean docs generate --type=llms
# Diffs that same path
protean docs generate --type=llms --check
An explicit --domain or --output on the command line overrides the config
value, so the key only fills in what the flags omit.
Both values are read relative to the file that declares them, so llms.txt
means the file next to that config, whether you run from the project root or
from a subdirectory. The command looks for the config in the directory you run
it from and its two parents, in the order .domain.toml, domain.toml,
pyproject.toml. A project created by protean new keeps its domain.toml
under src/<package>/, which a run from the project root does not see: put the
key in the root pyproject.toml there.
A config file that cannot be parsed stops the command with an error naming the file. Nothing else reads it on this path, so a typo there would otherwise drop the configured domain and output without a word.
The AGENTS.md constraint pack
--type=agents renders a versioned
AGENTS.md negative-constraint pack: the hard rules an agent
working on a Protean project must not break. It has one rule for every
error-level diagnostic code Protean can raise. Each rule is a prohibition built
from that code's registered meaning, rationale, and fix, and it ends with
the code so a reader can trace it back:
- **Do not** write code that causes this error: <meaning> <rationale> To
comply: <fix> (`CODE`)
Beyond the fixed wording in that template (the lead-in, To comply:, and the
code in backticks at the end), the generator adds nothing: it drops the three
registry fields in as they are written, joined with single spaces. The rule
reads as full sentences because each registry field already ends with a period.
Only error-level codes appear: advisory warning and info codes are style and
design nudges, not hard rules, so they are excluded. The set of rules is coupled
to the live registry, so adding or removing an error code changes the file.
--type=agents needs no --domain and no --ir: it derives only from the
registry and the installed Protean version, so it runs with no source. Like
llms and event-model, it is its own view and is not part of --type=all.
The output is byte-stable: for a given Protean version it renders identical bytes, because the header is version-stamped, the registry traversal is sorted, and nothing reads a timestamp.
This command prints the diagnostics-derived hard rules on their own. A project
generated with protean new ships a richer AGENTS.md: the dx-managed form,
which composes the packaged pack guidance with these hard rules inside a managed
block. That file is written and maintained by protean dx, not by this
command. After upgrading Protean, refresh it with protean dx refresh. Use this
command when you want the hard-rules pack by itself:
# Print the constraint pack
protean docs generate --type=agents
Annotating the event model
The event model carries structure, not the business context a person adds: why
a slice exists, the rule behind it, which team owns it. Those notes live outside
the generated output, in .protean/annotations.toml, and merge back in on
render. Keeping them out of the diagram means the model stays disposable and the
notes stay under version control next to the code they describe. The file's
shape is recorded in ADR-0032.
The file has a top-level [annotations] map keyed by element FQN. Each entry
carries a required note (free text, the business context) and an optional
owner (the team or person accountable). The FQN is the value
protean.utils.fqn computes, module.QualifiedName; in a project generated
from the canonical layout, an Order aggregate in
src/myproj/example/aggregate.py has the FQN myproj.example.aggregate.Order.
The FQN must be quoted, because TOML reads its dots as table separators
otherwise:
[annotations."myproj.example.aggregate.Order"]
note = """
Orders are the fulfillment boundary. An order cannot ship until payment
clears, so PaymentConfirmed gates the shipment slice.
"""
owner = "Fulfillment"
On render, each note merges into every slice that draws the element it keys. A note on an aggregate, a command, an event, or a consumer (a projector, event handler, or process manager) shows after that slice's Given-When-Then and before its diagram. A consumer drawn in two slices shows its note in both. Because the key is the element's FQN, a note stays attached across a content change (adding a field, reordering elements, regenerating the model) and breaks on an identity change (renaming or moving the element), which changes the FQN.
A key that matches no drawn element is listed in an "Unmatched annotations" section at the end of the render, so a note orphaned by a rename or a typo stays visible and gets re-keyed. A fact event is filtered from the event model, so it draws no node; a note keyed to one is reported unmatched.
--annotations <path> reads the file from a non-default location. An explicit
path that does not exist is an error, as is a malformed file: either aborts the
command before any output is written. With no annotations file present, the
default path absent and no --annotations given, the render is exactly what it
is without the feature.
# Merge notes from the default .protean/annotations.toml
protean docs generate --domain=my_app --type=event-model
# Read notes from a non-default path
protean docs generate --domain=my_app --type=event-model \
--annotations=docs/event-model-notes.toml
Notes are Markdown prose and cannot sit inside a raw flowchart, so
--format=mermaid leaves the diagram body unchanged and appends only the
unmatched-annotation report, the one piece that belongs at the end of the
render.
protean docs preview
The protean docs preview command starts a live preview server for Protean
documentation. This allows you to view changes in real-time as you edit.
Usage
protean docs preview [OPTIONS]
Options
--help: Shows the help message and exits.
Running a Preview Server
To start the live preview server for your project's documentation, run the command without any additional options:
protean docs preview
This starts a server on port 8000, reachable in a browser at
http://localhost:8000. It binds every interface (0.0.0.0), not just
loopback, so anyone who can reach the port sees your docs. Run it on a trusted
network. For a loopback-only preview, call mkdocs yourself:
mkdocs serve --dev-addr=127.0.0.1:8000. The exact URL is printed once the
server is running:
INFO - Building documentation...
INFO - Cleaning site directory
INFO - Documentation built in 0.56 seconds
INFO - [09:45:08] Watching paths for changes: 'docs', 'mkdocs.yml'
INFO - [09:45:08] Serving on http://0.0.0.0:8000/