Skip to content

HTTP API: entity

Every object of the layer as an addressable, cross-linked node: its route, its outgoing references, the references that resolve to it, the surfaces that answer for it, and what can be said about its enforcement without running anything. Nothing here enumerates kinds, entities or routes — an object of the index has a route because it is an object.

Rendered from docs/generated/openapi.json, which the executable generates from its capability registry and CI refuses stale. The raw document is at /openapi.json; a running server serves the same document at its own /openapi.json and Swagger UI at /swagger.

entity

Every object of the layer as an addressable, cross-linked node: its route, its outgoing references, the references that resolve to it, the surfaces that answer for it, and what can be said about its enforcement without running anything. Nothing here enumerates kinds, entities or routes — an object of the index has a route because it is an object.

GET /api/v1/entity entity.show

Read one entity

One object of the layer as an addressable node, by URI or by the address it is served at: identity and route, provenance, front matter and content, every reference it declares and every reference that resolves to it, the surfaces that answer for it, the state of the executable artefacts it names, and its public documentation page as the repository's site/data/publication.toml derives it (null when the repository declares none).

  • query
  • implemented
  • benchmark required
  • cache disabled
  • MCP tool majordomus_entity
  • $ entity show

Query parameters

nametyperequireddescriptionexamples
uri string no `majordomus://<kind>/<identity>`. Either this, or `kind` and `slug` together. first-object: "majordomus://adr/adr-0001"
kind string no The kind, when addressing by route.
slug string no The last segment of the entity's route, when addressing by route.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/entity?uri=majordomus%3A%2F%2Fadr%2Fadr-0001"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 EntityView Read one entity: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/entity/kinds entity.kinds

Every kind, and whether every object of it is addressable

The kinds the layer holds, each with the route of its index and how many objects it carries, and every route collision there is. A collision is the only way an object of the index can fail to have an address of its own; the healthy answer is none.

  • query
  • implemented
  • benchmark required
  • cache disabled
  • MCP tool majordomus_kinds
  • $ entity kinds

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/entity/kinds"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 KindList Every kind, and whether every object of it is addressable: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.

Schemas

The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.

ClaimedArtifact

object
One executable artefact an object names, and whether the tree holds it.

field is kept beside the path because the same object can name artefacts under several keys, and a reader fixing a dangling one needs to know which line of the front matter to look at rather than which file is missing.

use majordomus_cli::capability::builtin::entity::ClaimedArtifact;

let a = ClaimedArtifact {
    field: "x-majordomus.tests".into(),
    path: "test/cases/277_entities_are_routable.sh".into(),
    present: true,
};
let wire = serde_json::to_value(&a).unwrap();
assert_eq!(wire["field"], serde_json::json!("x-majordomus.tests"));
assert_eq!(wire["present"], serde_json::json!(true));
propertytyperequireddescription
field string yes The front matter key it was named under (x-majordomus.tests, test, ...).
path string yes The repository-relative path as named.
present boolean yes Whether the tree holds it.

Used by EntityEvidence

EntityDocumentation

object
The public documentation page of one entity, as its kind's publication decision derives it. Every surface that shows an entity carries this value and renders it; none of them derives a public address of its own.

use majordomus_cli::entity::Documentation;

let d = Documentation {
    projection: "entity".into(),
    route: Some("/adrs/adr-0056/".into()),
    url: Some("https://majordomus.dev/adrs/adr-0056/".into()),
    reason: None,
    declared_in: "site/data/publication.toml".into(),
};
let wire = serde_json::to_value(&d).unwrap();
assert_eq!(wire["url"], serde_json::json!("https://majordomus.dev/adrs/adr-0056/"));
// a field that does not apply is absent, not an empty string
assert!(wire.get("reason").is_none());
propertytyperequireddescription
projection string yes The kind's projection: entity, section or none.
route string | null no The public route of the page, site-relative: the entity's own page for an entity kind, the kind's section for a section kind, absent for none.
url string | null no The route under the site's base_url, when the site configuration names one.
reason string | null no Why the kind is not published, for none.
declared_in string yes The file the decision is read from.

Used by EntityView

EntityEdge

object
One edge, as an entity page reads it.

Everything a reader needs about the other end is here — what to show, what kind of thing it is, and the address to send them to — so the page renders a relation without going back to the index for it. route is None exactly when external is true: a file, a test or a command is named by the layer and is not an object of it, so there is no entity page to link to and the template shows plain text instead of a dead link.

use majordomus_cli::entity::{Direction, Edge};
use majordomus_cli::order::Ordered;

let edge = Edge {
    direction: Direction::Incoming,
    edge: "depends_on".into(),
    field: "depends_on".into(),
    uri: Some("majordomus://rule/project.x@1".into()),
    kind: "rule".into(),
    label: "project.x@1".into(),
    title: Some("A rule".into()),
    route: Some("/cockpit/objects/rule/project-x-1".into()),
    external: false,
};

// the relation is the group and the direction ranks inside it; see the `Ordered` impl
let key = edge.order_key();
assert_eq!(key.group, Some("depends_on"));
assert_eq!(key.rank, Direction::Incoming as i64);
assert_eq!(key.identity, "majordomus://rule/project.x@1");
propertytyperequireddescription
direction RelationDirection → RelationDirection yes Which way it runs from the entity that was asked for.
edge string yes The relationship, in the graph's vocabulary (depends_on, put_in_force, ...).
field string yes The front matter key the reference was declared under.
uri string | null no The object at the other end: its URI when the layer holds it.
kind string yes Its kind — of the layer, or the external vocabulary (file, test, command).
label string yes What to show: the identity of an object, or the name of something outside.
title string | null no Its title, when it is an object that has one.
route string | null no Its entity route, when it has one; absent for something outside the layer.
external boolean yes True when the other end is not an object of this index.

Used by EntityView

EntityEvidence

object
What can be said about an object's enforcement without running anything.

meaning travels with state so that every surface shows the same sentence about it: a badge alone invites each renderer to write its own gloss, and the gloss is where a "resolved" quietly becomes a "passing". A kind that names no artefact is Unclaimed with an empty artifacts, which is an answer and not an omission.

use majordomus_cli::capability::builtin::entity::{Evidence, EvidenceState};

let e = Evidence {
    state: EvidenceState::Unclaimed,
    meaning: "This object names no executable artefact.".into(),
    artifacts: Vec::new(),
    proof: None,
};

// the empty collections are omitted from the wire, so absence reads as absence
let wire = serde_json::to_value(&e).unwrap();
assert_eq!(wire["state"], serde_json::json!("unclaimed"));
assert!(wire.get("artifacts").is_none());
assert!(wire.get("proof").is_none());
propertytyperequireddescription
state EntityEvidenceState → EntityEvidenceState yes The state, which is never rounded up.
meaning string yes That state in one sentence, for a reader.
artifacts array of ClaimedArtifact → ClaimedArtifact no Every artefact named, with whether the tree holds it.
proof EntityProofRef | null → EntityProofRef no The capability that can say more, when this kind has one.

Used by EntityView

EntityEvidenceState

"unclaimed" | "dangling" | "resolved"
What this repository can say about the executable artefacts an object names, and only that. The word is chosen so that no reader can mistake it for a run.

The three states are ordered by how much they permit a reader to conclude, and the strongest of them still concludes nothing about a run: Resolved means the files an object points at exist. proof on Evidence is where "did it ever pass" is asked.

use majordomus_cli::capability::builtin::entity::EvidenceState;

// the wire spellings, which the Cockpit badge and the site template both read
for (state, word) in [
    (EvidenceState::Unclaimed, "unclaimed"),
    (EvidenceState::Dangling, "dangling"),
    (EvidenceState::Resolved, "resolved"),
] {
    assert_eq!(serde_json::to_value(state).unwrap(), serde_json::json!(word));
}
  • unclaimedThe object names no executable artefact. Most kinds name none, and that is not a defect: a prompt is not enforced by anything.
  • danglingIt names artefacts and at least one of them is not in the tree. A claim that points at nothing is worse than no claim, because it reads as enforcement.
  • resolvedEvery artefact it names is in the tree. This says the claim resolves — never that it passed. Whether it ever ran is a different question, and proof is where it is asked.

Used by EntityEvidence

EntityProofRef

object
Where a fuller answer about this entity's enforcement lives.

A pointer, never an answer. This module states what it can see in the tree and then names the capability that can say more, so that an entity page does not grow a second, weaker implementation of proof beside the one that already exists.

use majordomus_cli::capability::builtin::entity::ProofRef;

let p = ProofRef {
    capability: "rule.proves".into(),
    title: "What proves a rule".into(),
    address: "majordomus rule proves project.entities-are-routable@1".into(),
};
assert_eq!(
    serde_json::to_value(&p).unwrap()["capability"],
    serde_json::json!("rule.proves")
);
propertytyperequireddescription
capability string yes The capability that answers it.
title string yes Its title.
address string yes How to ask it here.

Used by EntityEvidence

EntitySurface

object
One place this entity can be reached, and how.

A row of the "where else this is answered" table on an entity page. surface is the interface's own name, so a reader who works on the command line and a reader who works over MCP each find their own line rather than a route they have to translate.

use majordomus_cli::entity::Surface;

let s = Surface {
    surface: "mcp".into(),
    address: "majordomus://rule/project.x@1".into(),
    detail: "the file as read".into(),
};
assert_eq!(
    serde_json::to_value(&s).unwrap()["surface"],
    serde_json::json!("mcp")
);
propertytyperequireddescription
surface string yes Which surface: mcp, http, cli, cockpit, site.
address string yes The address on it: a URI, a route, a command line.
detail string yes What a reader does with it.

Used by EntityView

EntityView

object
One object of the layer as an addressable node: what it is, where it is served, what it is joined to, and what can be said about its enforcement.

The whole answer in one value, because it is one question: every surface that shows an entity — the Cockpit page, GET /api/v1/entity, majordomus_entity, majordomus entity show — renders this and composes nothing of its own. A field a reader sees on one surface and not another would be a surface holding an opinion, which is what ADR 0004 forecloses.

use majordomus_cli::capability::builtin::entity::EntityView;

// it round-trips through the wire shape every projection reads it as
let view: EntityView = serde_json::from_value(serde_json::json!({
    "uri": "majordomus://adr/adr-0056",
    "id": "adr.adr-0056",
    "kind": "adr",
    "identity": "adr-0056",
    "slug": "adr-0056",
    "route": "/cockpit/objects/adr/adr-0056",
    "kind_route": "/cockpit/objects/adr",
    "title": "Every object of the layer has a derived address",
    "provenance": { "path": "x.md", "directory": "x", "source_class": "adr", "bytes": 0 },
    "metadata": {},
    "media_type": "text/markdown",
    "content": "",
    "relations": [],
    "surfaces": [],
    "evidence": { "state": "unclaimed", "meaning": "nothing named" },
    "documentation": {
        "projection": "entity",
        "route": "/adrs/adr-0056/",
        "url": "https://majordomus.dev/adrs/adr-0056/",
        "declared_in": "site/data/publication.toml",
    },
}))
.unwrap();

assert_eq!(view.route, "/cockpit/objects/adr/adr-0056");
// the Cockpit route is the object's; the public page is the site's, and both are named
let public = view.documentation.as_ref().and_then(|d| d.url.as_deref());
assert_eq!(public, Some("https://majordomus.dev/adrs/adr-0056/"));
// the kind's index is the entity's route with the last segment removed
assert!(view.route.starts_with(&view.kind_route));
propertytyperequireddescription
uri string yes majordomus://<kind>/<identity>.
id string yes The capability id, <kind>.<identity>.
kind string yes The kind.
identity string yes The identity within the kind.
slug string yes The last segment of its route, derived from the identity.
route string yes Its route in the Cockpit.
kind_route string yes The route of its kind's index.
title string | null no The title, when the kind's title rule found one.
description string | null no The one-line description, when the kind holds one.
provenance ObjectProvenance → ObjectProvenance yes Where it came from.
metadata any yes The parsed front matter or YAML, keys in the file's order.
media_type string yes IANA media type of content.
content string yes The file as read.
relations array of EntityEdge → EntityEdge yes The references it declares, and the references that resolve to it.
surfaces array of EntitySurface → EntitySurface yes Every surface that answers for it.
evidence EntityEvidence → EntityEvidence yes What can be said about its enforcement without running anything.
documentation EntityDocumentation | null → EntityDocumentation no Its public documentation page, derived from the repository's publication declaration (site/data/publication.toml) and the site's base_url by the same function the site generator writes the page with. Null when the repository declares no publication or does not declare this kind; a kind declared as not published carries the reason.

Used by entity.show

KindEntry

object
One kind of the layer, with its address and how many objects it holds.

example carries one of the kind's objects so that a catalogue of kinds is somewhere a reader can go from rather than a list of words: a kind with a count and no way in is a dead row. It is None only for a kind the index holds no object of.

use majordomus_cli::capability::builtin::entity::KindEntry;

let e: KindEntry = serde_json::from_value(serde_json::json!({
    "kind": "adr",
    "count": 52,
    "route": "/cockpit/objects/adr",
}))
.unwrap();

assert_eq!(e.kind, "adr");
// the route is the kind's, derived — never a per-kind entry in a table of routes
assert_eq!(e.route, majordomus_cli::entity::kind_route(&e.kind));
assert!(e.example.is_none());
propertytyperequireddescription
kind string yes The kind.
count integer (uint) yes How many objects of it the index holds.
route string yes Where its index is served.
example ObjectSummary | null → ObjectSummary no One of its objects, so a reader has somewhere to go.

Used by KindList

KindList

object
Every kind of the layer, each with its route, and every route collision there is.

The two numbers are what scripts/ci/entity-check compares: the objects the index holds against the objects that have an address. They are reported together, in one answer, so that the gate asks one question instead of joining two lists and deciding for itself what the difference means.

use majordomus_cli::capability::builtin::entity::KindList;

let healthy: KindList = serde_json::from_value(serde_json::json!({
    "count": 1,
    "kinds": [{ "kind": "adr", "count": 52, "route": "/cockpit/objects/adr" }],
    "routable": 52,
}))
.unwrap();

let held: usize = healthy.kinds.iter().map(|k| k.count).sum();
// every object addressable, and no collision: the only state the gate accepts
assert_eq!(held, healthy.routable);
assert!(healthy.collisions.is_empty());
propertytyperequireddescription
count integer (uint) yes How many kinds.
kinds array of KindEntry → KindEntry yes The kinds, in the index's order.
routable integer (uint) yes How many objects have a route. Every object does, unless it is in collisions.
collisions array of RouteCollision → RouteCollision no Two objects of one kind whose identities reduce to one route, which is the only way an object can fail to be addressable. Empty is the healthy answer and the only one scripts/ci/entity-check accepts.

Used by entity.kinds

RelationDirection

"outgoing" | "incoming"
Which way a relation runs from the entity being read.

The direction is a property of the reading, not of the relation: one edge in graph::RELATIONS is outgoing on the page of the object that declared it and incoming on the page of the object it names. Nothing in the layer writes Incoming down.

use majordomus_cli::entity::Direction;

// the wire spelling, which every surface shows and the site template reads
assert_eq!(
    serde_json::to_string(&Direction::Outgoing).unwrap(),
    "\"outgoing\""
);
assert_eq!(
    serde_json::to_string(&Direction::Incoming).unwrap(),
    "\"incoming\""
);
// declared before derived, which is the order an entity page renders the two sections
assert!((Direction::Outgoing as u8) < (Direction::Incoming as u8));
  • outgoingThis entity declared the reference.
  • incomingAnother entity named this one. Never declared: derived by reading every other object's references and keeping the ones that resolve here.

Used by EntityEdge

RouteCollision

object
Two objects of one kind whose identities reduce to the same route.

Carries both identities and both files, because the reader who has to fix it needs to know which two things collided and where they are written — a count would tell them only that the tree is refused. correction is the sentence collisions wrote about this particular pair.

#### use majordomus_cli::{Object, Provenance};
#### fn object(kind: &str, identity: &str) -> Object {
####     Object { kind: kind.into(), identity: identity.into(),
####         uri: format!("majordomus://{kind}/{identity}"), title: None, description: None,
####         metadata: serde_json::json!({}), body: String::new(), content: String::new(),
####         media_type: "text/markdown",
####         provenance: Provenance { path: format!("{identity}.md"), directory: "x".into(),
####             source_class: kind.into(), section: None, bytes: 0, member: None } }
#### }
use majordomus_cli::entity::{collisions, Collision};

// `a.b` and `a-b` are two identities and one slug
let objects = vec![object("rule", "a.b"), object("rule", "a-b")];
let found: Vec<Collision> = collisions(&objects);

assert_eq!(found.len(), 1);
assert_eq!(found[0].kind, "rule");
assert_eq!(found[0].slug, "a-b");
assert_eq!(found[0].identities, ["a.b", "a-b"]);
assert!(!found[0].correction.is_empty());
propertytyperequireddescription
kind string yes The kind they share.
slug string yes The slug they both reduce to; empty when an identity reduces to nothing.
identities array of string yes The identities that collide, in index order.
paths array of string yes The files that declared them.
correction string yes What to do about it.

Used by KindList

Searches the titles and descriptions of every page this site publishes.