Skip to content

HTTP API: devcontext

The context a development session should be given, derived from the repository rather than composed by hand: given an issue, a milestone, an intent or a set of paths, the canonical objects that bear on the work, why each one is in the answer, what was left out and why, what collapsed into what, and the whole cost against a budget. The selection is structured — every entry keeps its identifier, its provenance, the selector that reached it and its confidence — because rendering a prompt is a projection of the selection and not the selection itself.

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.

devcontext

The context a development session should be given, derived from the repository rather than composed by hand: given an issue, a milestone, an intent or a set of paths, the canonical objects that bear on the work, why each one is in the answer, what was left out and why, what collapsed into what, and the whole cost against a budget. The selection is structured — every entry keeps its identifier, its provenance, the selector that reached it and its confidence — because rendering a prompt is a projection of the selection and not the selection itself.

GET /api/v1/devcontext devcontext.compile

Compile the context for a piece of work

The objects a session working on this issue, milestone, intent or set of paths should be given: each with its canonical identifier, the index's own provenance, every discovery path that reached it with the reason and the confidence, its version, its tier and its cost in estimated tokens — followed by everything reached and not given with the reason for each, everything that collapsed into one entry, every pair that does not agree, and the per-tier spend against the budget. Deterministic for a given tree and request; the index fingerprint it was compiled from is in the answer.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=16)
  • MCP tool majordomus_devcontext
  • $ devcontext compile

Query parameters

nametyperequireddescriptionexamples
issue string no An issue id (`I0301`) or its canonical identifier. Its milestone, its dependencies, its declared scope and the decisions over that scope follow from it. issue: "I0001"
milestone string no A milestone id or slug, or its canonical identifier. milestone: "M000"
intent string no What the session is trying to do, in words. The only input the compiler infers from, and every entry it produces says so and carries a confidence below one. intent: "the context a session is given and the budget it costs"
paths array of string no Repository-relative paths the work touches, added to whatever the seeds declare.
uris array of string no Canonical identifiers to seed with directly, for a request about something that is neither an issue nor a milestone.
budget_tokens integer (uint64) no The ceiling in estimated tokens; `DEFAULT_BUDGET_TOKENS` when absent.
max_depth integer (uint) no How far from a seed the walk goes; `DEFAULT_MAX_DEPTH` when absent.
floor number (double) no Relevance below which an entry is reported rather than given; `DEFAULT_FLOOR` when absent.
all_blocking_rules boolean no Every blocking rule of the layer, not only the ones the work reaches. Honest and usually over budget: the answer then says `over_budget` rather than dropping what it may not drop.

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/devcontext?issue=I0001&milestone=M000&intent=the%20context%20a%20session%20is%20given%20and%20the%20budget%20it%20costs"

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 CompiledContext Compile the context for a piece of work: 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/devcontext/explain devcontext.explain

Why one thing is or is not in a compiled context

One canonical identifier judged under a request: whether it was selected, reached and excluded, folded into another identifier, held by the index and never reached, or unknown — with the entry, the exclusion or the collapse itself, and the budget the judgement was made under so that `excluded for budget` can be acted on without a second call.

  • query
  • behaviorally_verified
  • benchmark required
  • cache process (max_entries=8)
  • MCP tool majordomus_devcontext_explain
  • $ devcontext explain

Query parameters

nametyperequireddescriptionexamples
uri string yes The canonical identifier to explain. absent: "majordomus://rule/nothing-like-this@1"policy: "majordomus://policy/.ai/repo/policy.yaml"
issue string no An issue id (`I0301`) or its canonical identifier. Its milestone, its dependencies, its declared scope and the decisions over that scope follow from it.
milestone string no A milestone id or slug, or its canonical identifier.
intent string no What the session is trying to do, in words. The only input the compiler infers from, and every entry it produces says so and carries a confidence below one.
paths array of string no Repository-relative paths the work touches, added to whatever the seeds declare.
uris array of string no Canonical identifiers to seed with directly, for a request about something that is neither an issue nor a milestone.
budget_tokens integer (uint64) no The ceiling in estimated tokens; `DEFAULT_BUDGET_TOKENS` when absent.
max_depth integer (uint) no How far from a seed the walk goes; `DEFAULT_MAX_DEPTH` when absent.
floor number (double) no Relevance below which an entry is reported rather than given; `DEFAULT_FLOOR` when absent.
all_blocking_rules boolean no Every blocking rule of the layer, not only the ones the work reaches. Honest and usually over budget: the answer then says `over_budget` rather than dropping what it may not drop.

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/devcontext/explain?uri=majordomus%3A%2F%2Frule%2Fnothing-like-this%401"

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 Explanation Why one thing is or is not in a compiled context: 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/devcontext/policy devcontext.policy

The compiler's own rules

What the compiler decides and how: the tiers in the order the budget spends in with the kinds that land in each, every edge kind the composed graph declares with the relevance multiplier the compiler follows it by in each direction — or the reason it refuses to follow it at all — the selectors with which of them infer rather than read, and the defaults for the budget, the depth and the relevance floor. Read this rather than inferring the policy from an answer.

  • query
  • behaviorally_verified
  • benchmark required
  • cache disabled
  • MCP tool majordomus_devcontext_policy
  • MCP resource {"name": "devcontext-policy", "uri": "majordomus://devcontext/policy"}
  • $ devcontext policy

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/devcontext/policy"

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 CompilerPolicy The compiler's own rules: 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.

Budget

object
The budget, and what became of it.

over_budget is the field to read and the one most likely to be misread. It does not mean the compiler overspent by accident: every entry the budget has a say over is refused the moment it would not fit, so the only way past the ceiling is through the entries that may not be dropped — the seeds, the policy, the scope, a blocking rule. So over_budget means the things this context cannot do without already cost more than you allowed, and required_tokens is the explanation of it. The honest answer to that is to say so, not to return a context missing the thing it was compiled about.

remaining_tokens is floored at zero rather than going negative, so it is safe to display; the overspend is read from used_tokens against limit_tokens, which is why both are carried.

use majordomus_cli::devcontext::{Budget, Tier, BYTES_PER_TOKEN};
use serde_json::json;

let tier = |name: &str, tokens: u64, required: usize| json!({
    "tier": name, "meaning": "as this tier is for", "selected": 1,
    "excluded": 0, "tokens": tokens, "required": required,
});

// the undroppable entries alone cost more than the caller allowed
let b: Budget = serde_json::from_value(json!({
    "limit_tokens": 1_000,
    "used_tokens": 2_500,
    "required_tokens": 2_500,
    "remaining_tokens": 0,
    "over_budget": true,
    "bytes_per_token": BYTES_PER_TOKEN,
    "tiers": [tier("task", 2_000, 1), tier("governance", 500, 1)],
})).unwrap();

assert!(b.over_budget);
assert!(b.used_tokens > b.limit_tokens, "the overspend is read from these two");
assert_eq!(b.required_tokens, b.used_tokens, "all of it was undroppable");
assert_eq!(b.remaining_tokens, 0, "floored, so it is safe to display");

// the divisor is carried so a caller measuring tokens differently can convert
// rather than guess what the estimate meant
assert_eq!(b.bytes_per_token, BYTES_PER_TOKEN);

// and the tiers are reported in the order the budget spends in
let order: Vec<Tier> = b.tiers.iter().map(|t| t.tier).collect();
assert_eq!(order, [Tier::Task, Tier::Governance]);
assert!(order.windows(2).all(|w| w[0] < w[1]));
propertytyperequireddescription
limit_tokens integer (uint64) yes The ceiling this answer was compiled under, in estimated tokens.
used_tokens integer (uint64) yes What the survivors cost.
required_tokens integer (uint64) yes What the entries that cannot be dropped cost. Above the limit, the answer is over-budget and says so rather than dropping what it must keep.
remaining_tokens integer (uint64) yes limit - used, floored at zero.
over_budget boolean yes True when the required entries alone exceed the limit.
bytes_per_token integer (uint64) yes Bytes per token, the divisor the estimate uses.
tiers array of TierSpend → TierSpend yes Per tier, in the order the budget spends in.

Used by CompiledContext Explanation

CommitReference

object
One commit the layer itself names, and the record that named it.

The pair is the fact, not the commit alone. One commit named by three records is three references, because each record is a separate claim about what that commit was for, and folding them would lose the attribution — which is the only reason to carry a commit here at all. So named_by ends the ordering key, and two references to one commit both survive.

use majordomus_cli::devcontext::CommitReference;
use majordomus_cli::order::canonical;
use serde_json::json;

let by = |commit: &str, record: &str| -> CommitReference {
    serde_json::from_value(json!({"commit": commit, "named_by": record})).unwrap()
};

let mut commits = vec![
    by("cd04012", "majordomus://session/b"),
    by("cd04012", "majordomus://session/a"),
    by("5e31dd4", "majordomus://issue/I0301"),
];
canonical(&mut commits);

// filed under the commit, which is what a reader looks for
let order: Vec<&str> = commits.iter().map(|c| c.commit.as_str()).collect();
assert_eq!(order, ["5e31dd4", "cd04012", "cd04012"]);

// and the record ends the key, so one commit named twice stays two facts
assert_eq!(commits[1].named_by, "majordomus://session/a");
assert_eq!(commits[2].named_by, "majordomus://session/b");

// `covers` is what the record said the commit was for, when it said
let with_reason = by("cd04012", "majordomus://session/a");
assert!(with_reason.covers.is_none(), "absent rather than an empty sentence");
propertytyperequireddescription
commit string yes The commit as the record wrote it.
named_by string yes The entry that named it.
covers string | null no What the record says the commit was for.

Used by GitContext

CompiledContext

object
A compiled development context: what to give a session, what was left out, what collapsed into what, and the budget it was all decided under.

This is the canonical form. Rendering it as a prompt is a downstream projection of this value and is not performed here — the structure is what lets a caller re-budget, re-order or explain the selection without recompiling it.

Two invariants hold over every answer, and they are what make it safe to cache and to compare. fingerprint is over the index this was compiled from, so the same request against the same fingerprint is the same answer and a different fingerprint is a different tree — a caller can tell two trees apart without asking. And budget.used_tokens is exactly the sum of selected's costs, so the arithmetic in the budget describes the list beside it rather than a walk that has since been filtered.

use majordomus_cli::devcontext::{compile, CompileInput, CompiledContext};
use majordomus_cli::devcontext::{Tier, DEFAULT_BUDGET_TOKENS};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();

let c: CompiledContext = compile(&ctx, CompileInput::default()).unwrap();

// the answer says which tree it is true of, and nothing else has to be asked
assert_eq!(c.schema, 1);
assert!(!c.fingerprint.is_empty(), "an answer that names no tree is not one");

// the budget describes the list beside it
let spent: u64 = c.selected.iter().map(|e| e.cost_tokens).sum();
assert_eq!(c.budget.used_tokens, spent);
assert_eq!(c.budget.limit_tokens, DEFAULT_BUDGET_TOKENS);

// every tier is reported, so an empty tier is visibly empty rather than missing
assert_eq!(c.budget.tiers.len(), Tier::ORDER.len());

// a request that named nothing has no seeds and is still an answer
assert!(c.seeds.is_empty());

// and recompiling the same request over the same tree gives the same answer
let again: CompiledContext = compile(&ctx, CompileInput::default()).unwrap();
assert_eq!(again.fingerprint, c.fingerprint);
assert_eq!(again.selected.len(), c.selected.len());
propertytyperequireddescription
schema integer (uint64) yes The format of this answer.
fingerprint string yes The index this was compiled from, over every object's path and content. Two answers with the same fingerprint and the same request are the same answer; a different fingerprint is a different tree.
git GitContext → GitContext yes The head the answer is true of.
seeds array of Seed → Seed yes What was asked for.
selected array of ContextEntry → ContextEntry yes What to give the session, in the order the budget spent: tier, then relevance, then canonical identifier.
excluded array of Excluded → Excluded yes What was reached and not given, with the reason for each.
deduplicated array of Deduplicated → Deduplicated yes What collapsed into what.
conflicts array of Conflict → Conflict yes What does not agree with what.
budget Budget → Budget yes The budget and its per-tier spend.
diagnostics array of Diagnostic → Diagnostic yes What the compiler could not read: a knowledge source the layer names and does not hold, a session record that no longer describes this checkout. Never an error: an absent source is an answer.

Used by devcontext.compile

CompilerPolicy

object
The compiler's own rules, so that a caller reads the decision instead of inferring it from an answer.

This is the answer to "why did I get that", asked once rather than reverse-engineered from every context. It is a projection of the tables and constants this module is built from — Tier::ORDER, select::EDGES, Selector::ALL and the four defaults — and holds no numbers of its own, so the published rules and the code that applies them cannot drift apart.

The defaults are here because they are what a request gets when it says nothing, and a caller tuning a request needs to know what it is tuning away from.

use majordomus_cli::devcontext::{policy, CompilerPolicy, Selector, Tier};
use majordomus_cli::devcontext::{BYTES_PER_TOKEN, DEFAULT_BUDGET_TOKENS};
use majordomus_cli::devcontext::{DEFAULT_FLOOR, DEFAULT_MAX_DEPTH};
use majordomus_cli::graph;
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();
let rules: CompilerPolicy = policy(&ctx).unwrap();

// it names the graph it reads relations from, rather than leaving it to be guessed
assert_eq!(rules.schema, 1);
assert_eq!(rules.graph, graph::COMPOSED);

// the three tables are complete
assert_eq!(rules.tiers.len(), Tier::ORDER.len());
assert_eq!(rules.selectors.len(), Selector::ALL.len());
assert!(!rules.edges.is_empty());

// and the published defaults are the constants a silent request actually gets
assert_eq!(rules.default_budget_tokens, DEFAULT_BUDGET_TOKENS);
assert_eq!(rules.default_max_depth, DEFAULT_MAX_DEPTH);
assert_eq!(rules.default_floor, DEFAULT_FLOOR);
assert_eq!(rules.bytes_per_token, BYTES_PER_TOKEN);
propertytyperequireddescription
schema integer (uint64) yes The format of this answer.
graph string yes The graph the relations are read from.
tiers array of TierRule → TierRule yes The tiers, in the order the budget spends in.
edges array of EdgeRule → EdgeRule yes Every edge kind the composed graph has, followed or refused.
selectors array of SelectorRule → SelectorRule yes The selectors, and which of them infer.
default_budget_tokens integer (uint64) yes The default ceiling in estimated tokens.
default_max_depth integer (uint) yes The default depth limit.
default_floor number (double) yes The default relevance floor.
bytes_per_token integer (uint64) yes Bytes per estimated token.

Used by devcontext.policy

Conflict

object
Two things in the answer that do not agree, named rather than silently ordered.

The compiler could have resolved every disagreement by keeping one side, and for deduplication it does. A conflict is what is left when both sides are legitimately in the answer and they still do not agree — so the resolution belongs to the session reading it, and the compiler's job is to make sure the disagreement is visible rather than implied by the order of two rows.

current is the side the compiler treats as current, which is a statement about recency, not correctness: supersedes means the layer declared one to replace the other, version means two versions of one thing are both here.

use majordomus_cli::devcontext::Conflict;
use majordomus_cli::order::canonical;
use serde_json::json;

let row = |current: &str, against: &str, kind: &str| -> Conflict {
    serde_json::from_value(json!({
        "current": current, "against": against, "kind": kind,
        "detail": "both are in the answer and they disagree",
    })).unwrap()
};

let mut conflicts = vec![
    row("majordomus://rule/project.x@2", "majordomus://rule/project.x@1", "version"),
    row("majordomus://adr/0052", "majordomus://adr/0031", "supersedes"),
];
canonical(&mut conflicts);

// filed under the kind of disagreement, so like reads with like
let kinds: Vec<&str> = conflicts.iter().map(|c| c.kind.as_str()).collect();
assert_eq!(kinds, ["supersedes", "version"]);

// and both sides are always named: the pair is the fact, not the winner alone
for c in &conflicts {
    assert_ne!(c.current, c.against);
    assert!(!c.detail.is_empty());
}
propertytyperequireddescription
current string yes The entry the compiler treats as current.
against string yes The entry it stands against.
kind string yes What kind of disagreement: supersedes or version.
detail string yes The same, in words.

Used by CompiledContext

ContextEntry

object
One thing the compiler decided a session should be given: what it is, how it was reached, what it costs, and how far the compiler trusts it.

Notably, it does not carry the prose. The identifier is here and the body is not, because the body belongs to the object and objects.get serves it whole; copying it would put the layer in the answer twice and flatten the structure into text before anything had decided what to keep. An entry is a decision about an object, not a copy of one.

Three of the scalars are derived from discovered_by rather than independent of it, and a reader can check them: confidence is the highest of any path's, depth the shortest, and a discovered_by of length above one is the evidence that deduplication happened. required is the veto — a blocking rule, the policy, the scope, or a seed — and it is the one field that overrides the budget.

use majordomus_cli::devcontext::{tokens_for, ContextEntry, Selector, Tier};
use serde_json::json;

// one ADR, reached twice: once along a declared edge and once by matching the intent
let e: ContextEntry = serde_json::from_value(json!({
    "uri": "majordomus://adr/0052",
    "kind": "adr",
    "title": "The episode is the boundary",
    "provenance": {
        "path": ".ai/repo/adrs/0052.md", "directory": ".ai/repo/adrs",
        "source_class": "adr", "section": "adrs", "bytes": 8_192,
    },
    "tier": "decision",
    "relevance": 0.82,
    "confidence": 1.0,
    "discovered_by": [
        {"selector": "relation", "reason": "in force over this scope",
         "via": "majordomus://issue/I0301", "edge": "governed_by",
         "depth": 1, "confidence": 1.0, "weight": 0.9},
        {"selector": "intent_match", "reason": "the intent's words occur in it",
         "depth": 2, "confidence": 0.4, "weight": 0.4},
    ],
    "depth": 1,
    "cost_tokens": 2_048,
    "cost_bytes": 8_192,
    "version": "2026-09-11",
    "status": "accepted",
    "required": false,
})).unwrap();

assert_eq!(e.tier, Tier::Decision);

// the cost is the estimate over the measured bytes, and the answer carries both halves
assert_eq!(e.cost_tokens, tokens_for(e.cost_bytes));

// the derived scalars agree with the paths they are derived from
let best = e.discovered_by.iter().map(|d| d.confidence).fold(0.0_f64, f64::max);
assert_eq!(e.confidence, best);
assert_eq!(e.depth, e.discovered_by.iter().map(|d| d.depth).min().unwrap());

// two paths for one entry: the record that four things pointing here is one entry
assert_eq!(e.discovered_by.len(), 2);
assert_eq!(e.discovered_by[0].selector, Selector::Relation, "best path first");

// and the prose is not here: the identifier is how a caller gets the body
assert!(e.uri.starts_with("majordomus://"));
propertytyperequireddescription
uri string yes The canonical identifier: majordomus://<kind>/<identity>, unique in the answer.
kind string yes The kind the layer declared for it.
title string | null no Its title, when it has one.
provenance EntryProvenance → EntryProvenance yes Where it lives.
tier Tier → Tier yes Which tier's budget it is spent from.
relevance number (double) yes 0.0 to 1.0, deterministic for a given tree and request. The order within a tier.
confidence number (double) yes The highest confidence of any path that reached it.
discovered_by array of Discovery → Discovery yes Every path that reached it, best first. Length above one is the evidence that deduplication happened.
depth integer (uint) yes The shortest number of hops from a seed.
cost_tokens integer (uint64) yes Estimated tokens: the file's bytes divided by BYTES_PER_TOKEN, never fewer than one. An estimate, said to be one.
cost_bytes integer (uint64) yes The file's bytes, the measured half of the cost.
version string | null no The version or date the object states about itself, when it states one: a rule's version, an ADR's date, an issue's updated_at.
status string | null no The status word the object declares, when it declares one.
required boolean yes True when a blocking rule, a policy or the scope: never dropped for budget.
facts map of string no Facts worth carrying about this entry that are not the object's prose — a rule's class, an issue's milestone, the commits an issue's evidence names.

Used by CompiledContext Explanation

Deduplicated

object
Two names, one thing. The compiler keeps one entry and records the collapse here, so that "why is this ADR listed once when four things point at it" has an answer that is state rather than a log line.

One type covers two different collapses, and dropped is how a reader tells them apart. Structural collapse is the same identifier reached several ways: nothing was discarded, so dropped is empty and paths is the count that makes the row worth printing — this is the record that four things pointing at one ADR produced one entry. Semantic collapse is two different identifiers standing for one thing, one file indexed under two kinds or one rule at two versions: there dropped names what lost, because something really was discarded and a reader is entitled to know what.

use majordomus_cli::devcontext::Deduplicated;
use serde_json::json;

// structural: one identifier, four ways in, nothing discarded
let structural: Deduplicated = serde_json::from_value(json!({
    "kept": "majordomus://adr/0052",
    "dropped": [],
    "key": "uri",
    "detail": "one object reached by 4 discovery path(s)",
    "paths": 4,
})).unwrap();
assert!(structural.dropped.is_empty(), "nothing lost; this is a record, not a decision");
assert!(structural.paths > 1, "which is what makes the row worth printing");
assert_eq!(structural.key, "uri");

// semantic: two identifiers for one rule, and the older version lost
let semantic: Deduplicated = serde_json::from_value(json!({
    "kept": "majordomus://rule/project.x@2",
    "dropped": ["majordomus://rule/project.x@1"],
    "key": "identity",
    "detail": "one rule at two versions; the newer is in the answer",
    "paths": 1,
})).unwrap();
assert_eq!(semantic.dropped, ["majordomus://rule/project.x@1"]);
assert_ne!(semantic.kept, semantic.dropped[0], "two names, and one of them lost");
propertytyperequireddescription
kept string yes The entry that survived.
dropped array of string yes What was folded into it, or dropped for it.
key string yes What made them the same thing: uri, path or identity.
detail string yes The same, in words.
paths integer (uint) yes How many discovery paths reached the surviving entry.

Used by CompiledContext Explanation

DevContextStanding

"selected" | "excluded" | "deduplicated" | "not_reached" | "unknown"
What one identifier's standing in a compiled context is. Where one identifier stands with respect to a compiled context.

The pair to be careful about is NotReached and Unknown, because they look alike in a rendering and mean opposite things. NotReached says the index holds the object and this request did not reach it — the request is what to change, and raising the depth or the budget may well bring it in. Unknown says the index holds nothing under that identifier: the request is fine and the identifier is wrong, or the object is not in this repository. Collapsing the two would send a reader to tune a request that was never the problem.

Deduplicated is the third one worth naming: the thing is in the answer, under another identifier, which is neither in nor out.

use majordomus_cli::devcontext::Standing;

// in the answer, one way or another
let present = [Standing::Selected, Standing::Deduplicated];
// reached and not given, so the request is what to change
let request_bound = [Standing::Excluded, Standing::NotReached];
for s in present {
    assert!(!request_bound.contains(&s));
}

// the two that must not be confused: one is the request's fault, one is not
assert_eq!(Standing::NotReached.as_str(), "not_reached");
assert_ne!(Standing::NotReached, Standing::Unknown);
assert_eq!(
    serde_json::to_value(Standing::NotReached).unwrap(),
    serde_json::json!(Standing::NotReached.as_str()),
);
  • selectedIn the answer.
  • excludedReached and not given; the exclusion says why.
  • deduplicatedFolded into another identifier, or dropped for one.
  • not_reachedThe index holds it and this request never reached it.
  • unknownThe index holds nothing under that identifier.

Used by Explanation

Discovery

object
One way an entry was reached: which selector, why, how far from a seed, and how much the compiler trusts it. An entry keeps every one of these, because "five discovery paths found the same ADR" is the interesting fact, not a thing to hide.

A Discovery is a kind of way in, not one traversal. Thirty-three issues of one milestone all reach that milestone along belongs_to, and recording that thirty-three times would say the answer had thirty-three reasons when it has one — so a path already recorded for the same selector, edge and reason is kept rather than repeated.

via and edge are present exactly when they mean something: a seed was not reached from anything and followed no edge, so both are absent and are skipped on the wire rather than serialised as null. confidence is 1.0 for every declared selector and less only for Selector::IntentMatch; weight is what this particular path contributes to relevance, which is a different quantity — how much it counts, against how sure it is.

use majordomus_cli::devcontext::{Discovery, Selector};
use serde_json::json;

// the request named it: nothing to come via, no edge followed, depth zero
let seed: Discovery = serde_json::from_value(json!({
    "selector": "seed",
    "reason": "named in the request",
    "depth": 0,
    "confidence": 1.0,
    "weight": 1.0,
})).unwrap();
assert_eq!(seed.selector, Selector::Seed);
assert!(seed.via.is_none() && seed.edge.is_none());
assert!(seed.selector.declared() && seed.confidence == 1.0);

// the wire form omits them rather than carrying nulls a reader must skip
let wire = serde_json::to_value(&seed).unwrap();
assert!(wire.get("via").is_none() && wire.get("edge").is_none());

// a relation says both where it came from and which edge it followed
let walked: Discovery = serde_json::from_value(json!({
    "selector": "relation",
    "reason": "the decision in force over this scope",
    "via": "majordomus://issue/I0301",
    "edge": "governed_by",
    "depth": 1,
    "confidence": 1.0,
    "weight": 0.9,
})).unwrap();
assert_eq!(walked.edge.as_deref(), Some("governed_by"));
assert_eq!(walked.via.as_deref(), Some("majordomus://issue/I0301"));

// and a guess says so, in the one field that distinguishes the two halves of an answer
let guessed: Discovery = serde_json::from_value(json!({
    "selector": "intent_match",
    "reason": "the intent's words occur in it",
    "depth": 2,
    "confidence": 0.4,
    "weight": 0.4,
})).unwrap();
assert!(!guessed.selector.declared() && guessed.confidence < 1.0);
propertytyperequireddescription
selector Selector → Selector yes What reached it.
reason string yes Why it was included, in words a person reads without this file.
via string | null no The entry this one was reached from, when it was reached from one.
edge string | null no The edge kind followed, for Relation.
depth integer (uint) yes Hops from a seed; a seed is 0.
confidence number (double) yes 1.0 when the repository declared this path, less when it was inferred.
weight number (double) yes What this path contributes to the entry's relevance.

Used by ContextEntry

EdgeRule

object
One edge of the composed graph, and what the compiler does with it.

Every edge kind the graph declares gets a row, followed or not, which is the property that makes this projection worth reading: an edge added to the graph and not to the traversal table appears here as refused with a reason saying the compiler has no rule for it, rather than being silently unfollowed. A table that only listed the edges it followed could not tell you what it was ignoring.

forward and reverse are absent rather than zero when the edge is not followed that way, so "not followed" is one representation and not two. means is the graph's own account of the relation; reason is this compiler's account of why it is or is not context, in both directions.

use majordomus_cli::devcontext::{edge_policy, policy, EdgeRule};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();
let rules = policy(&ctx).unwrap();

// one row per edge kind the graph declares, followed or refused
let edges: &[EdgeRule] = &rules.edges;
assert!(!edges.is_empty(), "the graph declares edges");
for e in edges {
    // the row always says what the relation is and what was decided about it
    assert!(!e.means.is_empty() && !e.reason.is_empty(), "{}", e.edge);

    if e.refused {
        // not followed in either direction, and absent rather than zero
        assert!(e.forward.is_none() && e.reverse.is_none(), "{}", e.edge);
        assert!(edge_policy(&e.edge).is_none(), "{} is refused and in the table", e.edge);
    } else {
        // followed at least one way, at a weight the table declares
        assert!(e.forward.is_some() || e.reverse.is_some(), "{}", e.edge);
        let p = edge_policy(&e.edge).expect("a followed edge has a policy");
        assert_eq!(e.forward, (p.forward > 0.0).then_some(p.forward));
        assert_eq!(e.reverse, (p.reverse > 0.0).then_some(p.reverse));
    }
}
propertytyperequireddescription
edge string yes The edge kind, as the composed graph names it.
means string yes What the graph asserts by it.
forward number | null no The relevance multiplier when followed source to target; absent when refused.
reverse number | null no The multiplier when followed target to source; absent when refused.
reason string yes Why the target is context for the source, or why the edge is refused.
refused boolean yes True when the compiler will not follow it in either direction.

Used by CompilerPolicy

EntryProvenance

object
Where the selected thing lives, as the index knows it. Nothing here is authored: every field is the index's own crate::model::Provenance, carried rather than restated.

It is a copy of the index's provenance and not a re-derivation of it, which is the property worth relying on: no path here was constructed by this module, so a path in an answer is a path the index discovered, and the two cannot disagree about where something lives.

bytes is the measured half of an entry's cost — the estimate on top of it is tokens_for — so this is also where the budget's arithmetic starts.

use majordomus_cli::devcontext::{tokens_for, EntryProvenance};
use serde_json::json;

let p: EntryProvenance = serde_json::from_value(json!({
    "path": ".ai/repo/adrs/0052-the-episode-is-the-boundary.md",
    "directory": ".ai/repo/adrs",
    "source_class": "adr",
    "section": "adrs",
    "bytes": 8_192,
})).unwrap();

// the size is what the cost is computed from, and the estimate says so of itself
assert_eq!(tokens_for(p.bytes), 2_048);

// a file under no manifest section — a root README, say — carries none rather than ""
let rootish: EntryProvenance = serde_json::from_value(json!({
    "path": "README.md",
    "directory": ".",
    "source_class": "readme",
    "bytes": 4_000,
})).unwrap();
assert!(rootish.section.is_none());
assert_eq!(rootish.directory, ".", "the root is `.`, not the empty string");
propertytyperequireddescription
path string yes Repository-relative path, forward slashes.
directory string yes The directory it sits in.
source_class string yes The sources.yaml class that discovered it.
section string | null no The manifest section it falls under, when it falls under one.
bytes integer (uint64) yes Size of the file in bytes.

Used by ContextEntry

Excluded

object
One thing the compiler reached and did not give. Excluded is not the same as absent: an under-filled context is debugged from this list, not by guessing.

cost_tokens is why this type exists in the shape it does. A budget that reported only that it dropped things would leave a caller guessing at a new ceiling; one that reports what each omission would have cost turns "raise the budget" into arithmetic. detail carries the same in words, so the row is legible without the reader doing that arithmetic themselves.

The ordering puts the tier first as an explicit rank, because the tier is the precedence the budget spent in; inside a tier a reader is looking for a reason, and the identifier ends the key so the list is total and the same tree always prints it the same way.

use majordomus_cli::devcontext::{Excluded, ExclusionReason, Tier};
use majordomus_cli::order::canonical;
use serde_json::json;

let row = |uri: &str, tier: &str, reason: &str, cost: u64| -> Excluded {
    serde_json::from_value(json!({
        "uri": uri, "tier": tier, "reason": reason,
        "detail": "as measured", "cost_tokens": cost, "relevance": 0.4,
    })).unwrap()
};

let mut excluded = vec![
    row("majordomus://knowledge/b", "history", "budget", 900),
    row("majordomus://adr/0001", "decision", "superseded", 300),
    row("majordomus://knowledge/a", "history", "budget", 1_200),
];
canonical(&mut excluded);

// the tier ranks first, so the decision tier precedes history whatever the reason says
assert_eq!(excluded[0].tier, Tier::Decision);
assert_eq!(excluded[0].reason, ExclusionReason::Superseded);
// then the reason, then the identifier — which makes the order total
let rest: Vec<&str> = excluded[1..].iter().map(|e| e.uri.as_str()).collect();
assert_eq!(rest, ["majordomus://knowledge/a", "majordomus://knowledge/b"]);

// and the row a caller acts on says what it would have cost to keep
let biggest = excluded.iter().max_by_key(|e| e.cost_tokens).unwrap();
assert_eq!(biggest.cost_tokens, 1_200);
propertytyperequireddescription
uri string yes The canonical identifier, or the name the layer used when nothing resolves.
kind string | null no Its kind, when it resolved to something.
tier Tier → Tier yes The tier it would have been spent from.
reason ExclusionReason → ExclusionReason yes Why it is not in the answer.
detail string yes The same, in words.
cost_tokens integer (uint64) yes What it would have cost, so that a reader can decide whether to raise the budget.
relevance number (double) yes What its relevance was.

Used by CompiledContext Explanation

ExclusionReason

"budget" | "relevance" | "depth" | "stale" | "superseded" | "unresolved"
Why something the compiler selected did not survive.

The six divide into three that a caller can act on by asking again — Budget, Relevance and Depth are all consequences of the request's own limits, so raising the limit brings the entry back — and three that are facts about the repository: Stale and Superseded mean the layer itself says this is not the current thing, and Unresolved means the layer names something it does not hold, which is a dangling reference somebody should fix.

use majordomus_cli::devcontext::ExclusionReason as Why;

// the ones a different request would change, against the ones it would not
let request_bound = [Why::Budget, Why::Relevance, Why::Depth];
let repository_bound = [Why::Stale, Why::Superseded, Why::Unresolved];
for r in repository_bound {
    assert!(!request_bound.contains(&r), "{} is not the caller's to fix", r.as_str());
}

// and the word is the serialised form, snake_case for the two-word one
assert_eq!(Why::Superseded.as_str(), "superseded");
assert_eq!(
    serde_json::to_value(Why::Unresolved).unwrap(),
    serde_json::json!(Why::Unresolved.as_str()),
);
  • budgetThe budget ran out before its tier was filled.
  • relevanceIts relevance fell below the floor the request set.
  • depthIt is beyond the depth the request allows.
  • staleThe object says it is deprecated or superseded.
  • supersededA newer version of the same thing is in the answer.
  • unresolvedThe layer names it and holds nothing under that name.

Used by Excluded

Explanation

object
Why one identifier is, or is not, in a compiled context.

standing says which of the five cases this is, and the three optional fields are the evidence for it: at most one of entry, excluded and deduplicated is populated, and which one is determined by the standing rather than independent of it. detail is the same answer in words, so the value is legible without a reader matching them up.

budget is always present, including when the answer is unknown, and that is the point of carrying it: the commonest reason to ask this question is an entry that did not fit, and an explanation that said "excluded for budget" without the budget would need a second call to act on.

use majordomus_cli::devcontext::{compile, explain, CompileInput, ExplainInput};
use majordomus_cli::devcontext::{Explanation, Standing};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();
let compiled = compile(&ctx, CompileInput::default()).unwrap();
let selected = compiled.selected[0].uri.clone();

// something in the answer: the entry is the evidence, and the other two are empty
let e: Explanation =
    explain(&ctx, ExplainInput { uri: selected.clone(), ..Default::default() }).unwrap();
assert_eq!(e.standing, Standing::Selected);
assert_eq!(e.entry.as_ref().unwrap().uri, selected);
assert!(e.excluded.is_none() && e.deduplicated.is_none());
assert!(!e.detail.is_empty(), "the standing always reads as a sentence too");

// something this repository does not hold: an answer, not an error — and the budget
// the judgement was made under is carried even here
let absent: Explanation = explain(
    &ctx,
    ExplainInput { uri: "majordomus://adr/9999".into(), ..Default::default() },
)
.unwrap();
assert_eq!(absent.standing, Standing::Unknown);
assert!(absent.entry.is_none());
assert_eq!(absent.budget.limit_tokens, compiled.budget.limit_tokens);
propertytyperequireddescription
uri string yes What was asked about.
standing DevContextStanding → DevContextStanding yes Where it stands.
detail string yes The same, in words.
entry ContextEntry | null → ContextEntry no The entry, when it is in the answer.
excluded Excluded | null → Excluded no The exclusion, when it was reached and not given.
deduplicated Deduplicated | null → Deduplicated no The collapse it was part of, when it was folded or dropped for another.
budget Budget → Budget yes The budget the judgement was made under, so that "excluded for budget" can be acted on without a second call.

Used by devcontext.explain

GitContext

object
The head this answer was compiled against. A compiled context is only true of one tree, and these three fields plus the index fingerprint are what say which.

branch and head are optional because a checkout can legitimately have neither — a detached head, or a tree that is not a git repository at all — and the absence is carried rather than faked. working_tree is not optional, because there is always something to say: when git cannot be reached it carries unavailable: <reason> instead of a status, so a reader is never left unable to tell "clean" from "not asked".

commits is derived from the selected entries' own records and never from a log walk. That is the difference between "the commits this work says it produced" and "what happened to be committed recently", and only the first is context.

use majordomus_cli::devcontext::GitContext;
use serde_json::json;

// a tree the compiler could read
let known: GitContext = serde_json::from_value(json!({
    "branch": "master",
    "head": "cd04012f1b1dc21fd493e2be14938ffdfaccc8fa",
    "working_tree": "clean",
})).unwrap();
assert_eq!(known.branch.as_deref(), Some("master"));
assert!(known.commits.is_empty(), "no selected record named a commit");

// and one it could not: the status field still says something a reader can act on
let unknown: GitContext = serde_json::from_value(json!({
    "working_tree": "unavailable: not a git repository",
})).unwrap();
assert!(unknown.head.is_none() && unknown.branch.is_none());
assert!(
    unknown.working_tree.starts_with("unavailable: "),
    "never silently indistinguishable from clean",
);
propertytyperequireddescription
branch string | null no The branch, when the checkout is on one.
head string | null no The head commit.
working_tree string yes clean, dirty, or what git said.
commits array of CommitReference → CommitReference no Commits the selected entries name as their own evidence, newest record first. Derived from the layer's records, never from a log walk.

Used by CompiledContext

Seed

object
What the request asked to compile a context about.

A seed is the request echoed back after resolution, and it is in the answer so that a caller can see what their short name turned into. from names the request field it came from — issue, milestone, uris, paths, intent — which is how the same identifier arriving two ways stays two seeds rather than one.

resolved is populated only when resolution actually changed something: name a milestone by its slug and it carries the canonical identity, name it by that identity and there was nothing to resolve, so the field is absent. A path and an intent are not resolved against the index at all — their uri is the literal the request gave.

use majordomus_cli::devcontext::Seed;
use serde_json::json;

// named by a slug: the answer says what it became
let by_slug: Seed = serde_json::from_value(json!({
    "uri": "majordomus://milestone/M04",
    "kind": "milestone",
    "from": "milestone",
    "resolved": "M04",
})).unwrap();
assert_eq!(by_slug.resolved.as_deref(), Some("M04"));

// named canonically: nothing was resolved, so nothing is reported
let canonical: Seed = serde_json::from_value(json!({
    "uri": "majordomus://issue/I0301",
    "kind": "issue",
    "from": "issue",
})).unwrap();
assert!(canonical.resolved.is_none());

// and a path is carried verbatim: its `uri` is not a canonical identifier at all
let path: Seed = serde_json::from_value(json!({
    "uri": "apps/majordomus-cli/src",
    "kind": "path",
    "from": "paths",
})).unwrap();
assert!(!path.uri.starts_with("majordomus://"));
assert_eq!(path.from, "paths", "which request field it came from");
propertytyperequireddescription
uri string yes The canonical identifier of the seed, or the literal the request gave for an intent or a path.
kind string yes issue, milestone, path, intent, or the kind of the object named.
from string yes Where it came from: which field of the request.
resolved string | null no What the compiler resolved it to, when the request named something by a short name.

Used by CompiledContext

Selector

"seed" | "relation" | "scope_path" | "contract" | "governance" | "session_state" | "intent_match"
What reached an entry. This is the provenance of the selection, and it is a different fact from the provenance of the file: Relation says the compiler followed an edge the layer declares, IntentMatch says it guessed from words. An answer that did not keep them apart could not say which half of itself to trust.

The division that matters is Selector::declared, and it is deliberately lopsided: six of the seven selectors follow something the repository states, and exactly one guesses. That ratio is the design. Inference is confined to a single selector so that the inferred part of any answer can be found, weighed and ignored, rather than being mixed through the rest at unmarked strength.

use majordomus_cli::devcontext::Selector;

// exactly one selector infers, and the answer can always be split on it
let inferred: Vec<&str> = Selector::ALL
    .iter()
    .filter(|s| !s.declared())
    .map(|s| s.as_str())
    .collect();
assert_eq!(inferred, ["intent_match"]);

// every selector has its own word, so a reason is never ambiguous
let mut words: Vec<&str> = Selector::ALL.iter().map(|s| s.as_str()).collect();
let before = words.len();
words.sort();
words.dedup();
assert_eq!(words.len(), before);

// and the word is the serialised form
assert_eq!(
    serde_json::to_value(Selector::ScopePath).unwrap(),
    serde_json::json!("scope_path"),
);
  • seedNamed in the request.
  • relationReached along a typed edge of the composed graph.
  • scope_pathIts path lies under a path the request or a seed declares.
  • contractA directory contract that reaches one of those paths.
  • governanceA rule or policy the layer applies to everything.
  • session_stateA recorded session of this branch or of these paths, the handover the resolver chose, the newest checkpoint.
  • intent_matchInferred: the words of the intent occur in the object's own words.

Used by Discovery SelectorRule

SelectorRule

object
One selector, and whether what it produces was declared or inferred.

Two fields, and the second is the whole reason the projection exists: a caller consuming a compiled context needs to know which selectors it can trust as repository fact, and deriving that from the answer would mean inferring it from confidence values. Published here, it is a rule rather than a pattern a reader noticed.

use majordomus_cli::devcontext::{policy, Selector, SelectorRule};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();
let rules = policy(&ctx).unwrap();

// every selector is published, and each row agrees with the selector it names
let selectors: &[SelectorRule] = &rules.selectors;
assert_eq!(selectors.len(), Selector::ALL.len());
for r in selectors {
    assert_eq!(r.declared, r.selector.declared());
}

// and the inferring one is named, so the guessed half of an answer can be found
let inferred: Vec<Selector> =
    selectors.iter().filter(|r| !r.declared).map(|r| r.selector).collect();
assert_eq!(inferred, [Selector::IntentMatch]);
propertytyperequireddescription
selector Selector → Selector yes The selector.
declared boolean yes Declared by the repository, or inferred by the compiler.

Used by CompilerPolicy

Tier

"task" | "governance" | "source" | "decision" | "knowledge" | "history"
Where an entry sits in the order the budget spends in.

The order is this repository's own: lib/context.sh assembles its sections "in authority order, highest first, because a worker that runs out of budget must lose the least reliable evidence rather than the most: git, then the task and its policy, then blockers, then authored records, then event history." These six tiers are that sentence, applied to objects instead of to sections.

The derived Ord is load-bearing: the variants are declared in authority order, so < means "spent before" and "harder to drop", and the budget's ordering is a plain sort rather than a table mapping tiers to ranks. A variant added in the wrong place would silently re-rank the whole answer, which is why the declaration order is the one thing about this enum to be careful with.

use majordomus_cli::devcontext::Tier;

// the declaration order *is* the order the budget spends in
let mut sorted = Tier::ORDER.to_vec();
sorted.sort();
assert_eq!(sorted, Tier::ORDER.to_vec());

// so the strongest authority is the smallest, and it is the one never dropped
assert_eq!(Tier::ORDER.iter().copied().min(), Some(Tier::Task));
assert!(Tier::Task < Tier::Governance && Tier::Governance < Tier::History);

// and the serialised word is the lowercase one the report is read under
assert_eq!(
    serde_json::to_value(Tier::Governance).unwrap(),
    serde_json::json!(Tier::Governance.as_str()),
);
  • taskWhat was asked for: the issue, its milestone, the paths named, the intent. Never dropped — a context that lost the thing it was compiled about is not a smaller answer, it is the wrong one.
  • governanceWhat the work will be judged against: the policy, the scope, the directory contracts that reach the named paths, and the rules. A blocking rule is never dropped.
  • sourceThe code and the cases the work touches.
  • decisionThe architectural decisions already in force over it.
  • knowledgeWhat a previous session left: handovers, session records, curated knowledge.
  • historySupporting evidence: use cases, claims, features, and the rest of the layer.

Used by ContextEntry Excluded TierRule TierSpend

TierRule

object
One tier, as the policy reports it: its place in the spend order, what it is for, and which kinds of object land in it.

position is 1-based and exists because the wire form has no inherent order a consumer can rely on — the Tier enum's Ord is a Rust fact, not a JSON one — so the rank is made explicit for the readers that are not this crate. kinds is the inverse of tier_for_kind, published so that a caller can see where a kind will land without compiling an answer and looking.

use majordomus_cli::devcontext::{policy, tier_for_kind, Tier, TierRule};
use majordomus_cli::synthetic::SyntheticRepository;

let repo = SyntheticRepository::small().unwrap();
let ctx = repo.context().unwrap();
let rules = policy(&ctx).unwrap();

// the first tier spent is the one that is never dropped, at position 1
let first: &TierRule = &rules.tiers[0];
assert_eq!(first.tier, Tier::Task);
assert_eq!(first.position, 1, "1-based, so the order survives serialisation");

// every tier is reported, in the order the budget spends in, and explains itself
assert_eq!(rules.tiers.len(), Tier::ORDER.len());
for (i, r) in rules.tiers.iter().enumerate() {
    assert_eq!(r.position, i + 1);
    assert_eq!(r.meaning, r.tier.meaning());
}

// and every kind is listed under the tier the classifier puts it in, exactly once —
// so the published table and the function behind it cannot disagree
let mut listed = 0usize;
for r in &rules.tiers {
    for kind in &r.kinds {
        assert_eq!(tier_for_kind(kind), r.tier, "{kind} is listed under the wrong tier");
        listed += 1;
    }
}
let mut all: Vec<&str> = rules.tiers.iter().flat_map(|r| &r.kinds).map(String::as_str).collect();
all.sort();
all.dedup();
assert_eq!(all.len(), listed, "a kind is listed under one tier only");
propertytyperequireddescription
tier Tier → Tier yes The tier.
position integer (uint) yes Its position in the order the budget spends in; 1 is spent first.
meaning string yes What it is for.
kinds array of string yes The kinds of object that land in it.

Used by CompilerPolicy

TierSpend

object
What one tier cost, how much of it survived, and how much of that could not have been dropped.

There is no per-tier ceiling, which is worth saying because the name suggests one: the budget is a single pot spent in tier order, and this is the report of what each tier took from it, not an allowance it was given. A tier with many exclusions is therefore not a tier that hit its own limit — it is a tier that was reached after the pot had been spent by the tiers above it.

meaning is carried so that a reader deciding what to raise the budget for does not have to look up what knowledge holds; required is the part of selected the budget had no say over.

use majordomus_cli::devcontext::{Tier, TierSpend};
use serde_json::json;

let t: TierSpend = serde_json::from_value(json!({
    "tier": "governance",
    "meaning": Tier::Governance.meaning(),
    "selected": 9,
    "excluded": 2,
    "tokens": 6_400,
    "required": 3,
})).unwrap();

assert_eq!(t.tier, Tier::Governance);
// the undroppable part is a part of what survived, never a separate count
assert!(t.required <= t.selected);
// and the row explains its own tier, so "2 excluded" is actionable
assert_eq!(t.meaning, Tier::Governance.meaning());
assert!(t.meaning.contains("policy"));
propertytyperequireddescription
tier Tier → Tier yes The tier.
meaning string yes What it is for.
selected integer (uint) yes How many entries survived in it.
excluded integer (uint) yes How many were dropped from it for budget.
tokens integer (uint64) yes Tokens the survivors cost.
required integer (uint) yes Entries in it that cannot be dropped.

Used by Budget

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