How the Rust executable under apps/majordomus-cli/ exposes
what it exposes: one canonical declaration per capability, modules that compose
capabilities, a root that composes modules, the registry built from them, one executor
every transport calls, and the projections (MCP, HTTP, OpenAPI, Swagger UI, the command
line, the benchmark targets, the cache behaviour, the generated reference and manifests)
that are derived from the registry and define nothing of their own. Behaviour as
implemented and tested; where this document and the executable disagree, the document is
wrong and changes in the same commit. The decisions are
ADR 2 (the registry and the
projections), ADR 4
(modules, the executor, benchmarks as evidence) and
ADR 5
(one projection plan, named owners, the site as a view of the registry); the rules are
project.interfaces-are-projections, project.rust-canonical-declaration,
project.rust-benchmark-coverage and project.rust-hot-path.
ONE CANONICAL DECLARATION capability! { id, kind?, title, description, input, output,
stability, exposure, tags, cache?, benchmark?, handler }
↓
MODULE COMPOSITION module! { id, title, description, stability, capabilities: [...] }
↓
ROOT COMPOSITION compose_modules![repository, objects, capabilities, graph, health, peers, perf]
↓
CAPABILITY REGISTRY + every declarative object of the layer, validated, frozen, fingerprinted
↓
DERIVED PROJECTIONS MCP · HTTP · OpenAPI → Swagger UI · CLI · the Cockpit
· the execution plane and its live channel
· benchmark targets and coverage · cache policy · perf counters
· docs/generated/* · the website's /registry/ pages
A contributor adding one capability edits one capability! block (with its typed input
and output and the input's benchmark cases) and runs majordomus generate. Nothing else.
A contributor adding a command has one more thing to say, and only one: whether it is
the projection of a capability (a CliExposure on that capability's declaration) or belongs
to the command line alone, in which case cli::LOCAL in
src/cli/local.rs carries the reason and
majordomus quality report checks it. A command that says neither is a gate failure rather
than an operation quietly missing from the API — the rule is
project.operation-transport-parity and the working reference is QUALITY.md.
What is canonical, what is derived, what is not authoritative
CANONICAL DERIVED (projections) NOT AUTHORITATIVE
capability! blocks, one per module file MCP tools and resources examples in prose
apps/majordomus-cli/src/capability/ HTTP routes screenshots
builtin/<module>.rs, composed by OpenAPI document the committed snapshots
module! and compose_modules! Swagger UI configuration under docs/generated/
declarative objects of the layer capabilities list/describe (caches of the registry)
.ai/** as sources.yaml maps them the Cockpit's pages, a latency number in prose
how each kind is read and validated navigation, search, palette, (evidence lives under
share/kinds.yaml, share/schemas/*.json runner form and graphs .ai/local/benchmarks/ and
.ai/repo/knowledge/kinds.yaml, schemas/ benchmark targets, coverage .ai/repo/benchmarks/rust/)
the regression policy cache behaviour (executor) the Cockpit's own markup
.ai/repo/benchmarks/rust/policy.yaml perf counters (a projection, never a
the graph derivations docs/generated/* place a fact is stated)
graph::DERIVATIONS, from the registry share/allow/*.txt (shell tool)
and the index the website's /registry/ pagesflowchart TD B[typed executable descriptors<br/>capability/builtin.rs] --> R[CapabilityRegistry] D[declarative objects<br/>.ai/** via sources.yaml] --> I[index] --> R K[share/kinds.yaml + share/schemas<br/>+ repository additions] --> I R --> M[MCP tools and resources] R --> H[HTTP routes] R --> O[OpenAPI document] --> S[Swagger UI] R --> C[capabilities list / describe] R --> K2[the Cockpit<br/>pages, navigation, runner, graphs] R --> G[docs/generated] --> W[the website's /registry/ pages] K --> A[share/allow/*.txt]
A change to one descriptor, one declarative file, one kind or one schema reaches every
projection on the next start or the next majordomus generate; nothing is edited twice.
The Cockpit (COCKPIT.md, ADR 12) is the projection a person reads. It is on
this list rather than beside it: its pages are laid out from what a capability answered
through the same executor every other transport calls, its navigation catalogues are the
registry's modules and the index's kinds, its runner's form is generated from the input
schema, and its examples are the capability's own benchmark cases. Nothing in it names a
capability, a kind, a route or a graph.
The model
A capability is a descriptor with:
| field | meaning |
|---|---|
id | the canonical identity: a namespace, a dot, an opaque local part (repository.info, rule.majordomus.scope-integrity@1, document.docs/CLI.md); unique across both sources |
kind | query: executable, read-only, one typed handler; command: executable, one typed handler, changes this process's own memory and nothing else (a peer announcing itself), bound to POST and announced to MCP clients as not read-only; resource: declarative content, read as it is. No kind writes to the repository, and how long a call takes is not a kind |
execution | what running it as an execution means: the effect and whether two may overlap, classified from the kind; and whether asking it to stop achieves anything, which only its handler can say and which .cancellable() on the declaration declares (EXECUTIONS.md) |
title, description | the words every projection shows |
input, output | canonical JSON Schemas; for a query, derived from its Rust types; for a resource, the object view |
provenance | builtin with the module, or declarative with the repository-relative path, directory, source class, section and, for a member of a collection file, the member's key path |
exposure | explicit, per projection: mcp (tool name and/or resource URI), http (method and path under /api/v1/), cli (the words after majordomus); absent means not exposed there, and nothing infers one |
stability | implemented, behaviorally_verified, experimental, planned, unsupported; a planned or unsupported capability may be listed and is never executable through any projection |
Id grammar: the namespace matches [a-z][a-z0-9_-]*; the local part is non-empty with no
whitespace or control character, any other Unicode included; ids are compared as strings,
case-sensitively. Declarative ids are <kind>.<identity>, where the identity is what the
kind's identity rule produced: id@version for a rule, name for a prompt, the
repository-relative path for a kind without identity fields.
The registry
Built at one place per process, from the builtin executables composed explicitly in
capability/builtin.rs and from every object of the index. It refuses to build, naming
every party, on a duplicate id (Rust with Rust, Rust with declarative), a duplicate MCP
tool name or resource URI, a duplicate HTTP route, a duplicate CLI path, a malformed
exposure (a route outside /api/v1/, a tool name outside [a-z0-9_]+), a query or a
command without a handler, a resource with one, a command with an MCP resource exposure
or an HTTP method other than POST, a query exposed as an MCP resource whose input
requires anything (a read supplies none), and an executable exposure on a planned or
unsupported capability. Errors are collected, not stopped at the first. majordomus capabilities validate runs exactly this and exits 10 with the list.
Modules and composition
A capability lives in the Rust module of its namespace under
apps/majordomus-cli/src/capability/builtin/: repository.rs, objects.rs,
capabilities.rs, peers.rs, perf.rs. Each file declares its typed inputs and
outputs, implements BenchmarkCases for every input type, writes one handler per
capability, and ends with module():
pub fn module() -> ModuleDescriptor {
module! {
id: "objects", title: "Objects", description: "...", stability: Stability::BehaviorallyVerified,
capabilities: [
capability! { id: "objects.list", ... handler: objects_list },
capability! { id: "objects.search", ..., cache: CachePolicy::Process { max_entries: 64, ttl_seconds: None }, handler: objects_search },
],
}
}
builtin/mod.rs composes the application, and that line is the only root composition:
pub fn modules() -> Vec<ModuleDescriptor> {
compose_modules![repository, objects, capabilities, peers, perf]
}
The macros are macro_rules! that build plain values; app.rs hands them to the
registry builder explicitly. There is no procedural macro, no global registry filled
behind the caller's back, no link-time inventory and no build script reading src/. The
registry stamps nothing it did not receive: module! stamps its id on each capability,
and a capability whose id namespace is not its module (ModuleMismatch), a module
composed twice, an invalid module id, a cache policy that keeps nothing, a cached
command or a benchmark policy that contradicts the kind refuses the build, naming the id
and the provenance. Executables composed without a descriptor (tests, benchmarks) get a
module derived from their namespace; declarative objects get their kind. The registry's
summary counts modules, required and waived benchmark targets and cached executables,
and capabilities validate prints them.
The executor and the cache
Every call goes through one path, whatever asked for it: Context::execute →
CapabilityExecutor::execute → the handler. The stdio session, /mcp, the HTTP routes,
the capabilities commands and the benchmark runners own protocol conversion and nothing
else, so instrumentation and caching apply to every transport at once. The executor
counts executions, handler invocations, cache hits, misses and evictions in the
process-wide perf::COUNTERS, beside the counters of the work that happens once
(repository scans, index builds, registry builds, schema generations, MCP projection
builds, OpenAPI builds, HTTP router builds) and phase timings on a monotonic clock;
perf.counters answers them over every transport, which is how the structural tests prove
that hundreds of requests rebuild nothing.
A cache is policy on the descriptor: CachePolicy::Process { max_entries, ttl_seconds }.
The key is the canonical id, the input in canonical form (object keys sorted at every
level, so a client's key order never matters) and the registry fingerprint, a sha-256 of
the index fingerprint (every object's path and content) and of every descriptor, so two
repository states never share an entry. Errors are never cached; a command is never
cached and the registry refuses a descriptor that asks; the bound evicts the oldest entry
first; nothing is persisted. Two capabilities declare it because a measurement said so
(objects.search and capabilities.list); every other handler answers from the
immutable index in microseconds and a cache there would be a second copy of nothing. The
generic tests iterate every cached capability with every case and with generated inputs:
uncached, cold and warm agree, a hit runs no handler.
Benchmarks: every operation, a generated denominator
The benchmark projection derives its targets from the registry against a repository:
each executable with a required policy, directly and on every transport its exposure
declares, once per case its input type provides; plus the transports' own operations,
declared once as system targets (a cold majordomus mcp process, initialize, ping,
tools/list, resources/list, resources/read; GET /, /openapi.json, /swagger).
Coverage is covered / required with the denominator computed, never typed: a required
capability whose input type produced no case for this repository is missing, and
capabilities validate, bench coverage --check and CI fail on it; a waiver is a typed
reason on the descriptor, reported and never counted.
majordomus bench coverage [--format json] [--check] # covered / required, per transport and in total
majordomus bench [id] [--transport direct|mcp|http|system] [--profile quick|full|ci] [--format json] [--no-write]
majordomus bench --check # against this platform's baseline, under the policy
majordomus bench baseline update [--profile full] [--allow-dirty]
The runners time a target directly through the executor (cold, the cache cleared before
every sample, and warm, for a cached capability; handler invocations counted), over a
real loopback socket served by the same process with the input bound as the route binds
it (query string for GET, JSON body for POST), and through a real majordomus mcp --standalone child on stdio (one process, many samples; a fresh process per sample for
the process-cold target). Statistics: samples, min, p50, p90, p95, p99, max, mean,
stddev. A run is a document, majordomus/benchmark-result/v1, with the commit, the dirty
state, the build profile, the platform and the registry fingerprint, written under
.ai/local/benchmarks/; the accepted baseline is one tracked file per platform under
.ai/repo/benchmarks/rust/baseline.<os>-<arch>-<build>.json, promoted only by bench baseline update (a dirty tree refuses without --allow-dirty); the regression policy is
.ai/repo/benchmarks/rust/policy.yaml (relative thresholds per metric, an absolute
floor under which a difference is noise, per metric the sample count under which it
does not gate, so that a quick run fails on its median only and a full run on every
percentile, and per-target allowances for the targets the machine's load dominates, such
as the process-cold spawn). bench --check reports every line, names new
targets and stale baseline entries (a renamed capability is never silently matched), and
notes when the registry fingerprint moved. A baseline is compared on its own platform
only, and it fails a run only on the host that recorded it: the result document carries
the host (CPU brand string and logical core count), and a run on another host, such as a
hosted CI runner reading a baseline recorded on a developer's machine, prints every line
with "reporting only" and exits 0. A CI runner without a committed baseline compares
nothing and says so.
Kinds and schemas, read at run time
Which files are declarative objects, and of which kind, is the repository's:
.ai/repo/knowledge/sources.yaml maps pathspecs to kinds. How a kind is read is data
too, in the tool distribution's share directory (--share, MAJORDOMUS_SHARE, the
repository's own share/, or the one beside the executable):
share/kinds.yaml— per kind: the format (markdown,yaml,text), whether front matter is required, which JSON Schema the metadata must satisfy, which fields carry identity, title and description, a version field with its supported values, and for a collection file the list that holds the members.declared:names the kinds a Markdown file may declare for itself through its front matter (contexttoday).share/schemas/<name>.schema.json— one JSON Schema (draft 2020-12) per contract; validated with a JSON Schema validator on every read. A key the schema does not allow is the diagnosticunknown_key; any other failed constraint isschema_violation, naming the path and the constraint.
A repository extends both under its knowledge section: .ai/repo/knowledge/kinds.yaml
adds kinds, .ai/repo/knowledge/schemas/<name>.schema.json adds schemas. Adding is the
only operation: a kind or a schema the distribution already declares is an error naming
both files. The shell tool's allow-lists under share/allow/ are generated from the
schemas that carry x-majordomus-allow (majordomus generate allow); they are never
written by hand.
Projections
| projection | derived from | where |
|---|---|---|
| MCP tools | capabilities with an mcp.tool exposure; inputSchema and outputSchema are the canonical schemas; _meta.majordomus.id carries the id; readOnlyHint follows the kind | majordomus mcp on stdio, and /mcp on the shared server |
| MCP resources | capabilities with an mcp.resource exposure; a query with one is read as JSON | majordomus mcp on stdio, and /mcp on the shared server |
| HTTP routes | capabilities with an http exposure; GET binds every top-level input property as a query parameter coerced by its schema type, POST binds the JSON body (a command's binding); errors map to 400 invalid_input, 404 not_found, 422 refused, 500 internal, 405 for another method on a known path | the shared server majordomus mcp starts, and majordomus serve |
| OpenAPI 3.1 | the same routes; operationId is the id; the tags are the modules with their descriptions; every example is one of the capability's benchmark cases, by name, evaluated against the repository's index; the responses are the statuses the router answers for the kind (422 for a command only) and default for what the transport adds; a query parameter is never nullable; x-majordomus-id, -kind, -stability, -provenance, -benchmark, -cache, -mcp, -cli carry the rest; info, licence, contact and externalDocs from about.rs and the crate manifest; schemas hoisted into sorted components; the OAS 3.1 base dialect | GET /openapi.json, docs/generated/openapi.json, and the site's /docs/api/ and /openapi.json |
| Swagger UI | a shell page that loads /openapi.json; it embeds no specification; its assets come from the pinned swagger-ui-dist on unpkg, the one part that is not offline | GET /swagger (/docs is the documentation) |
| command line | capabilities list, describe and projections dispatch through the registry's cli exposure; schema and validate are views of the registry, not capabilities | majordomus capabilities … |
| projection closure | the registry's cli exposures against the clap declaration, both ways: every claim answered by a runnable command, and the commands no capability claims | capabilities projections: majordomus_projections, GET /api/v1/capabilities/projections; the projection line of capabilities validate |
| reference | the index of modules and builtin capabilities, one page per executable module with every capability in full; declarative resources described by rule, listed live; the command line as clap declares it | docs/generated/capabilities.md, docs/generated/modules/<id>.md, docs/generated/cli.md, docs/generated/cli.{json,yaml} (majordomus/cli/v1) |
| benchmark targets | every required executable per exposed transport per case, plus the system targets; the coverage tallies | majordomus bench, docs/generated/benchmarks.md and docs/generated/benchmarks.{json,yaml} (majordomus/benchmark-matrix/v1) — one computation, three encodings |
| registry manifest | the builtin registry as data: modules, descriptors with schemas and the file each was composed in, declarative kinds, system targets; the boundary the site generator reads for its routes | docs/generated/registry.{json,yaml} (majordomus/capability-registry/v1) |
| artifact manifest | the generation plan itself: every artifact with the document it projects, its encoding, the schema its content satisfies, its source, size and hash | docs/generated/artifacts.{json,yaml,md} (majordomus/generated-artifacts/v1); read back by artifacts.list — majordomus_artifacts, GET /api/v1/artifacts, majordomus://artifacts, /cockpit/artifacts, and the site's /registry/artifacts/ |
| executions | every executable capability can be started as an execution, watched over the live channel and read back; the descriptor's execution policy decides what a client may offer, and nothing is registered a second time | majordomus run, executions.*, GET /events, /cockpit/executions (EXECUTIONS.md) |
| perf counters | the executor's and the startup phases' counters | perf.counters: majordomus_perf, GET /api/v1/perf |
| allow-lists | the schemas | share/allow/*.txt, each under a # provenance banner every reader of one skips |
| site dataset | the registry (fingerprint, counts, every builtin descriptor in full with its source file, every module with its ids), the index (fingerprint, every object without its content), the kinds, the declared provider projections, the command line, the MCP tools and resources, the HTTP routes, the benchmark targets, coverage, policy and accepted baselines; no timestamps of its own, no absolute paths, no git state | site/data/registry/registry.json (majordomus-site-registry/v2) — majordomus generate site; rendered under /registry/ (overview, executable, modules, capabilities, cli, mcp, benchmarks) |
| provider bootstraps | the policy's projections[], the profiles and the provider templates (.ai/repo/providers/, else share/providers/); the stamp carries the policy hash and the content hash; byte-identical to the shell tool's update | AGENTS.md, CLAUDE.md, GEMINI.md, … — majordomus generate providers |
What every projection says about itself comes from about.rs: the OpenAPI info,
the MCP initialize instructions and the HTTP index (GET /) open with the same
summary and carry the same paragraphs, so no interface describes the surface in words
of its own. The infrastructure routes /, /openapi.json, /swagger and /mcp are the HTTP
projection's own and are not capabilities; /mcp is MCP over HTTP (the Streamable HTTP
transport's request half, with Mcp-Session-Id sessions) and exists on the shared server
only. One shared server serves a repository: the first majordomus mcp or serve binds
it, every later majordomus mcp bridges its stdio to it, and the peers see each other
through peers.list; the lifecycle is in MCP.md.
Order is a projection too
A collection has one order, and every surface renders the sequence it was handed. The order
is apps/majordomus-cli/src/order.rs: a total order over four parts, most significant
first — the semantic group (none sorts last), an explicit rank for the collections
whose domain declares one (weight on a moment, an audience, an area, a feature), the
label compared naturally (digit runs by value, so item-2 precedes item-10; ASCII
case folded, so Alpha and alpha stay adjacent), and the canonical identity.
The identity is what makes the order total rather than merely tidy. Two items with the same label must not exchange places because an unrelated item was added, because a different iterator was used, or because another machine enumerated differently.
To join it, implement order::Ordered on the type — beside the type, not beside a
renderer — and call order::canonical(). Nothing else is edited: every projection that
shows the collection shows the new sequence.
To validate it, run scripts/ci/order-check. Three of its checks are absolute — no
case-folded comparator outside order.rs, no sort key that calls render(), no
.localeCompare( under scripts/, share/ or site/ — and one is a ratchet over the debt
that predates the rule, held as two counts in .ai/repo/order-baseline.txt: sort sites in
the crate outside order.rs, and shell sort invocations not pinned with LC_ALL=C. Both
may fall and may not rise; a commit that adopts the canonical order lowers the baseline with
scripts/ci/order-check --update.
Grouping is the order's first part, and it is derived too. A capability module's area is
the areas of the features that name it in modules:, resolved by the catalogue's own
weight — lowest first, ties by id. No module declares an area and no file lists the pairs;
the Cockpit's sidebar asks the product model. Two features that name one module and share no
area disagree about what it is for: the resolution stays deterministic and the disagreement
is reported as contested_area by majordomus product validate, to be settled in the
feature file rather than by a tiebreak. ADR 0026 records why the parent is derived rather
than declared.
To diagnose an unexpected sequence, read the key rather than the output. A collection
ordered somewhere other than order.rs has an opinion of its own; a collection whose key
ends before its identity has ties, and a tie is where an order looks like a race when there
is none. The rule is project.canonical-order; ADR 0025 records the decision.
The one projection that can drift
Every projection in the table above is built by walking the registry. An MCP tool, an HTTP route, an OpenAPI operation, a benchmark target and a reference page cannot exist without a descriptor, and a descriptor cannot fail to produce one: there is nothing to compare, because there is only one declaration.
The command line is different. It is declared a second time, in clap, in
apps/majordomus-cli/src/cli.rs, because clap owns parsing, --help, defaults and value
sets, and deriving that from the registry would mean reimplementing an argument parser to
avoid writing a path twice. CliExposure is therefore not a projection — it is a claim
about a declaration that lives somewhere else, and two declarations can disagree.
They did. repository.scope_classify declared cli: ["scope", "classify"] so that the
command module could find its id with by_cli. There is no such subcommand: majordomus scope classify parses classify as a path to judge and answers out undeclared classify [absent]. The claim was repeated by capabilities describe, by docs/generated/cli.md and
by the site's registry dataset, and nothing noticed, because the only test of the CLI
projection asked the registry whether it agreed with itself.
capability/closure.rs compares the two declarations:
- a claim the command line does not answer is a failure —
CLOSURE_CLI_ABSENTfor a path clap does not have,CLOSURE_CLI_NOT_RUNNABLEfor one that only groups other commands. The finding names the capability, the claim, the file that declares it, the file that declares the command line, and the command that shows it again. - a runnable command no capability claims is not a failure.
serveandmcpstart processes,generatewrites files, and the worktree verbs call a service directly; none of them is a capability, and some never will be. The list is reported as the measure of how much of the command line is still hand-written rather than derived.
It is a pure function of the registry and the clap tree — no repository, no environment, no
network — so it runs in a unit test, in tests/projections.rs, and as the projection line
of capabilities validate, which CI runs through scripts/rust-check --integration.
majordomus capabilities projections answers the same matrix over the command line, over
HTTP and over MCP, with --unmet for the failures alone.
Lifecycle and failure policy
discover the repository -> locate the share directory -> load kinds and schemas
-> discover files through sources.yaml -> read and validate each into an object or a diagnostic
-> build the registry (refuse on any invariant) -> construct the projection asked for -> serve or generate
The manifest, sources.yaml, the kinds files and the schemas are errors when they cannot
be read: nothing can be discovered without them. Every other file that cannot become an
object is excluded with a diagnostic naming its path and a stable code, the index reports
degraded, and the projections still serve; --strict refuses a degraded index. A
registry invariant violation is an error: no projection is served from a registry that
does not build.
Extension
Adding an object of a known kind
Write the file where the repository's sources.yaml class for that kind looks, with the
front matter or YAML its schema allows; track it (or run with --discovery filesystem);
restart. It is a resource capability, an MCP resource, a member of objects.list, readable
through objects.get over MCP and HTTP (one resolution of the URI, shared with the MCP
resource read, which also answers majordomus://repository as repository.info's
report), and listed by capabilities list. No Rust, no
registration, no projection edited. apps/majordomus-cli/tests/external_extension.rs adds,
removes and breaks objects between restarts and reads them back through every interface.
Adding a kind, with its schema, from the repository
.ai/repo/knowledge/kinds.yaml schema: majordomus-kinds/v1 + one kinds: entry
.ai/repo/knowledge/schemas/note.schema.json its JSON Schema
.ai/repo/knowledge/sources.yaml a class mapping a pathspec to the kind
Then the objects. The same test proves it for a kind named note. A kind needing a new
format or identity rule is a Rust change in index.rs; data describes objects, it does
not define how a format is read.
Adding an executable capability
In the file of its module under apps/majordomus-cli/src/capability/builtin/:
- define the typed input and output (
serde+schemars::JsonSchema; doc comments are the descriptions every client reads) and implementBenchmarkCasesfor the input (one or more representative inputs; a case may look at the index to name an object that exists), - write one function
fn(&Context, Input) -> Result<Output, CapabilityError>; the context carries the index, the registry, the peer board, the executor and, through an MCP session, the calling peer, - add one
capability! { id, title, description, input, output, stability, exposure, tags, handler }block to the module'scapabilities: [...], withkind: CapabilityKind::Commandafter the id when it changes this process's memory,cache: CachePolicy::Process { .. }when a measurement says so, and.cancellable()after the block when the handler looks atctx.progress.cancelled()and stops, - run
majordomus generateandmajordomus capabilities validate(orjust generateandjust validate); commit the regenerated files underdocs/generated/, - add a behavioural test of the handler's semantics.
That is the whole workflow. MCP, HTTP, OpenAPI, Swagger UI, the capabilities commands,
the benchmark targets on every exposed transport (with the cases from step 1), the cache
behaviour, perf.counters, the reference, the benchmark matrix and the registry manifest
follow from the block; the generic suites (tests/projections.rs, tests/bench.rs,
tests/executor.rs, tests/properties.rs, tests/hot_path.rs) discover the capability
through the registry and test it without an edit. A capability whose input type has no
BenchmarkCases does not compile; one whose cases are empty for a repository fails
coverage.
Adding a module
One Rust module under builtin/ with its module() built by module!, and one name
added to compose_modules! in builtin/mod.rs. Its reference page, its rows in the
matrix and its entry in the manifest are generated.
Generated projections and synchronization
majordomus generate [all|openapi|docs|benchmarks|registry|allow|providers|site|manifest]
writes every artifact of the selected targets; majordomus generate --check derives them
again, compares byte for byte, writes nothing, and exits 10 naming every stale file. CI
runs the check. The site generator consumes registry.json, openapi.json and
artifacts.json and nothing else of the crate; scripts/derive runs the two generators in
dependency order and scripts/derive-check composes both checks
(docs/GITHUB_PAGES_ARCHITECTURE.md). The committed files are caches: reviewable, never
edited. What is generated is not written down here — docs/generated/artifacts.md is that
list, and it is generated.
A derivation refuses an executable of another generation. Every artifact is a
projection of the model compiled into the executable that writes it, so a binary built from
another revision of the crate rewrites all of them from a model the tree does not have —
silently, because nothing in the bytes says which executable produced them. A version
string does not separate the two: on 2026-09-10 a binary calling itself the same version,
passed in through MAJORDOMUS_BIN, reported eleven artifacts stale on a pristine
origin/master and rewriting them removed 16,625 lines at exit 0. So generate asks, of a
repository that declares this crate, three questions in turn and proceeds on the first one
answered: the version must agree, because it is stamped into every provenance header;
the generation — the digest generation::crate_generation takes over
apps/majordomus-cli/{Cargo.toml,Cargo.lock,build.rs,src}, compiled in by build.rs — may
equal the tree's, which means this executable was built from these sources; or the
model it projects may equal the builtin half of the registry the tree committed at
docs/generated/registry.json — the declarative kinds there are the repository's content,
not the executable's model — which is what keeps a correctly-matching prebuilt
MAJORDOMUS_BIN working without a rebuild. Otherwise it refuses with REFUSED (15) and
writes nothing. Nothing is asked at all of a repository that does not declare this crate: a
released binary generating a foreign repository is what a released binary is for. The verdict is deliberately not
CONTRACT_UNMET (10): that is what a genuinely stale artifact reports, and its remedy —
run the generator — is exactly what destroys the tree when the executable is the thing
out of date. scripts/derive and scripts/derive-check propagate the two apart.
Every artifact is typed. It declares the document it projects, the encoding it is
written in (json, yaml, markdown, text, matching its own suffix), the schema its
content satisfies when the document has a contract, and the source it was derived from.
The rule is project.generated-artifacts-are-typed@1;
generate and generate --check verify the plan before a byte is written or compared and
refuse a half-typed tree.
A structured document is written in every encoding it is committed in, from one value.
generate::Document holds the value; the JSON and the YAML are two renderings of it, never
two computations, and a document whose audience includes a reader also has a Markdown
rendering of the same value. That is why the benchmark matrix in benchmarks.md cannot
disagree with benchmarks.json.
Every artifact carries its provenance in the form its encoding allows: schema,
generated and generator as members in JSON; x-majordomus-generated and
x-majordomus-generator where the document's own specification fixes its member names (the
OpenAPI document); a # comment banner in YAML and in line-oriented text; an HTML comment
in Markdown. A provider bootstrap carries the majordomus update stamp of the policy it was
rendered from instead. None carries a timestamp, an absolute path or a fingerprint that
would move with a document edit.
A document that names a schema has a published contract. They live under
share/schemas/generated/, one file per document, pinned to it by the const of its
schema member — kind-schema discovery does not recurse, so nothing there is ever read as
an object kind's schema. generate validates every document against its contract.
The whole set is indexed by docs/generated/artifacts.{json,yaml,md}, itself generated:
every artifact with its encoding, contract, source, size and hash. Its own three encodings
carry no hash — a document that hashed itself would have no fixed point — and --check
compares them byte for byte instead. artifacts.list reads that manifest back and
reconciles it with the working tree, which is what the MCP tool, the HTTP route, the
Cockpit's Artifacts page and the site's /registry/artifacts/ all show.
When something fails
| message | meaning | remedy |
|---|---|---|
capability 'x' is defined twice: <a> and <b> | two sources claim one id | rename one, or delete the duplicate |
MCP tool 'n' is claimed by 'a' and 'b', HTTP route GET /p is claimed by …, CLI path … is claimed by … | two capabilities project to one name | change one exposure |
invalid HTTP exposure: path '/x' is not under /api/v1/ | a route outside the versioned prefix | move it under the prefix |
declares the execution policy …; its kind makes it … | a descriptor carries a policy that is neither its kind's nor its kind's with cancellation | let the declaration classify itself, or add .cancellable() |
… is planned and cannot be exposed as executable through MCP tool | a planned capability declares an executable exposure | drop the exposure until it is implemented |
unknown_key … not in schema 'rule': owner | a declarative file carries a key its schema does not allow | remove the key, or extend the schema in the repository's schemas/ for a repository kind |
schema_violation … class: "fatal" is not one of … | a value fails a constraint | fix the value |
kind 'x' is declared by both share/kinds.yaml and .ai/repo/knowledge/kinds.yaml | a repository redefines a distributed kind | rename the repository's kind |
generated artifact(s) stale: docs/generated/openapi.json (differs) | a committed projection no longer matches the registry | run majordomus generate and commit |
this executable is not of this tree's generation | the executable was built from another revision of the crate and projects another model | rebuild it, or run scripts/derive with MAJORDOMUS_BIN unset — not majordomus generate, which is what rewrites the artifacts from the wrong model |
capability 'x.y' (builtin …) is composed in module 'z' but its namespace is 'x' | a capability! block sits in the wrong module's list | move it to the module its id names, or rename the id |
module 'x' is composed twice | two module! share an id, or a builtin module's id is a declarative kind | rename one |
invalid cache policy: a process cache with max_entries 0 keeps nothing, … a command changes state and is never cached | the descriptor's cache policy contradicts itself or the kind | fix the policy on the descriptor |
FAIL benchmarks N of M requirement(s) missing | an exposed executable's input type produced no case for this repository | make BenchmarkCases return a case (or waive with a typed reason, which is reported) |
bench --check: regression(s) found | a metric grew over .ai/repo/benchmarks/rust/policy.yaml against this platform's baseline | find the cause with majordomus bench <id> and the phase timings, or record a new baseline deliberately |
STALE <key> (in the baseline, not measured …) | the baseline knows a target this run did not measure: renamed, removed, or filtered out | bench baseline update after a rename or removal |
no share directory holds kinds.yaml; tried … | the distribution was not found | pass --share or set MAJORDOMUS_SHARE |
Stability
| status | |
|---|---|
| the registry's invariants, both sources, deterministic build | behaviourally verified (tests/registry.rs) |
| every declared projection present, no orphan, one id everywhere; a change to one descriptor reaches MCP, OpenAPI and the reference | behaviourally verified (tests/projections.rs) |
| HTTP over a real socket, OpenAPI, Swagger shell, typed errors, MCP and HTTP answering the same handler identically, a declarative object reaching HTTP and introspection untouched | behaviourally verified (tests/http_serve.rs) |
| every route replayed over a socket with the capability's own benchmark cases, and the document showing those cases as examples, the modules as tags, the statuses of the kind as responses, the policies as extensions, one prose with MCP and the index | behaviourally verified (tests/http_serve.rs, test/cases/92_openapi_reference.sh); timed by benches/routes.rs |
the site's API reference rendered from the committed document, the raw document served at /openapi.json, externalDocs pointing at the page | checked by scripts/generate-site-data --check and scripts/site-check (test/cases/92_openapi_reference.sh) |
one shared server per repository: the lease, the bridge, /mcp sessions, the peers and their announcements, the fallback port, serve deferring, --standalone, the takeover after a kill, the re-attachment, the refusal when the taker cannot serve | behaviourally verified (tests/mcp_shared.rs, tests/shared_units.rs, test/cases/90_mcp_shared_server.sh) |
| repository-defined kind and schema served without a code change; add, remove, break | behaviourally verified (tests/external_extension.rs) |
generate, byte-identical regeneration, --check on missing and tampered files | behaviourally verified (tests/generate_check.rs) |
| modules compose capabilities, the root composes modules, the module invariants | behaviourally verified (tests/registry.rs, test/cases/91_canonical_architecture.sh) |
| one executor; cache off, cold and warm agree; a hit runs no handler; errors and commands never cached; the bound; the fingerprint | behaviourally verified (tests/executor.rs, tests/properties.rs) |
| no request rebuilds canonical state after startup (counters over hundreds of real requests) | behaviourally verified (tests/hot_path.rs, test/cases/91_canonical_architecture.sh) |
| every operation a benchmark target, generated denominator, exposure and policy propagation, real runners, baseline check | behaviourally verified (tests/bench.rs, tests/bench_units.rs, test/cases/91_canonical_architecture.sh) |
| generated reference per module, benchmark matrix, registry manifest, deterministic and reconciled | behaviourally verified (tests/projections.rs) |
the whole path through the shell tool's own init | behaviourally verified (test/cases/76_capabilities_projections.sh) |
the id grammar, URIs, tool names, route paths, diagnostic codes, kinds.yaml, the schema files | implemented; pre-1.0 compatibility surfaces, changes documented, never silent |
Swagger UI assets offline or on the site, response examples, /openapi.yaml, path parameters, hot reload, mutation of the repository over any interface, a server-initiated stream on /mcp | not implemented; restart-based rediscovery is the contract, and a shared server keeps the index it built at start until its last client leaves |