Skip to content

protean docs

The protean docs command group has two subcommands:

  • protean docs generate renders architecture documentation from a domain or an IR JSON file.
  • protean docs preview starts 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 versioned llms.txt context pack (see below).
    • agents: a versioned AGENTS.md negative-constraint pack (see below).
    • all (default): every section except event-model, llms, and agents.
  • --format, -f: markdown (fenced code blocks, the default) or mermaid (raw diagram source). mermaid is not supported for catalog, llms, or agents.
  • --output, -o: Write to a file instead of stdout.
  • --cluster: Filter to a single cluster FQN (only with --type=clusters or --type=all).
  • --annotations: Path to an annotations TOML file (only with --type=event-model). Defaults to .protean/annotations.toml when 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:

  1. Command(s) that trigger the aggregate.
  2. Aggregate (state) that decides and holds state.
  3. Event(s) the aggregate raises (fact events are omitted).
  4. 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/end lifecycle, 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/