pub const SANDBOX_STATUS: &str = "Report what the machine-global SANDBOX IMAGE STORE holds: one row per cached container \
image, with its reference, digests, object counts and a size breakdown. MACHINE-GLOBAL, \
and `scope` says so: one store per asset root, shared by every repository here, so never \
attribute a size to the project under discussion. `bytes.total` is what an image \
references; `bytes.exclusive` is what dropping that image alone would free — they differ \
when images share a layer, so quote `exclusive` when saying what clearing one would give \
back. Extracted trees and disk images are a cache below this one, built on first run, so \
a pulled-only image is complete without them. `preserved` is state no pinned digest \
re-obtains, which `sandbox_clear` never removes. Read this before `sandbox_clear` and show \
the user the numbers: a destructive verb with no way to see what it will destroy is \
invoked blind. Read-only.";
pub const SANDBOX_CLEAR: &str = "DELETE cached container images from the machine-global sandbox image store, and report \
what that freed. The one tool here that changes anything, and it cannot reach findings, \
memory or the graph: everything it drops is re-obtainable from a pinned digest, so it \
costs a re-download and never information. MACHINE-GLOBAL: one store per asset root, \
shared by every repository this server hosts, so clearing for one project slows the next \
sandboxed run for all. Call `sandbox_status` first and show the user what is cached and \
what it costs — a re-pull is minutes to tens of minutes and gigabytes. `image` and \
`everything` are DIFFERENT REQUESTS with no default: pass exactly one, `image` with a \
reference from `sandbox_status` or `everything: true`. Report what it \
freed: quote `freed_bytes`, with `store_bytes_before`/`store_bytes_after` either side, \
rather than saying it worked. `retained` re-checks every surviving image against the disk \
afterwards; if any `complete` is false say so prominently — that is a damaged store, not \
a successful clear, and `roteiro security prefetch` is the repair.";
pub const SECURITY_LIST: &str = "List the SECURITY FINDINGS stored for this repository: every live findings layer, the run \
evidence behind it, and a page of its findings. READ `coverage` FIRST: \
`no-analyzer-on-record` means nothing was \
analyzed and is NOT a clean repository — it carries NO `report` at all, so say so rather \
than report zero findings; `analyzed` with `findings` 0 is the other case, an analyzer \
that ran and found nothing. `limit` is 1-100 (default 20) — no unlimited setting — and is findings PER LAYER; \
each layer carries its true `findings` count beside the `page` returned. A page keeps the most severe findings first, so what is \
omitted is the least severe — never conclude a severity is absent from a truncated page. \
`cross_reference` is a view over those findings, not a replacement: it groups dependency \
advisories both analyzers reported, `confirmed_by` counts how many, `1` is normal rather \
than a discrepancy, and the `findings` total is unchanged by it. Read-only: it cannot run \
an analyzer or ingest a report — ask the user to run `roteiro security run` or `roteiro \
security ingest`, because a tool call is not a person consenting to execution.";
pub const SECURITY_STATUS: &str = "Report SECURITY READINESS in TWO SEPARATELY SCOPED SECTIONS; report them separately, never \
merged. `machine`: this HOST — its pinned-asset cache and each analyzer's coverage matrix \
with `host_readiness`. Identical for every project here, and \
says nothing whatsoever about whether anything has been run; `ready` is readiness to run \
ON THIS HOST and no more. `host_readiness` has THREE states with different remedies: \
`ready` (assets provisioned AND the analyzer's program on PATH); `assets-not-provisioned` \
(ask the user to run `roteiro security prefetch`); `binary-not-found` (ROTEIRO NEVER \
INSTALLS ANALYZERS — ask the user to install it or to `roteiro security ingest` a report \
from elsewhere). `assets_provisioned` and `missing_programs` are ALWAYS present, so when \
the state is not `ready` read both: a host can lack both, and `host_readiness` names only \
the first remedy. The sandboxed backend supplies analyzers from a digest-pinned image, so \
`binary-not-found` \
does not block it, and this tool does not inspect the image store, so it reports no \
sandbox verdict. `repository` describes ONE PROJECT: which findings layers are live, how \
many findings each holds, how old the advisory data behind each is. \
`possibly_stale: true` whenever advisory data is involved and NEVER means current; `false` \
means only that there is no advisory axis. Read `repository.coverage` first: \
`no-analyzer-on-record` means nothing has been analyzed and is NOT a clean repository. \
COUNTS, NEVER FINDINGS; use `security_list` for those, and it needs no `limit`. Read-only: \
it cannot provision, and \
`roteiro security prefetch` needs human consent, so ask the user to run it.";
pub const LIST_TOOL_CLASSES: &str = "Name this server's tool CLASSES — `query`, `quality`, `security`, `sandbox` — the tools in \
each, and which are LOADED here. Call it before telling a user Roteiro cannot do \
something: a class can be left out at startup to keep its descriptions out of every \
turn's prompt, and `not-loaded-here` means not advertised to this session, NOT a missing \
capability. Report the class name so the user can restart the server with it. Read-only.";
pub const CHECK: &str = "Run the AUTHORED-LAYER DRIFT CHECK — the same gate `roteiro check` exits non-zero on and \
the pre-commit hook reads — and return its verdict as data: unresolvable ADR \
`[[path#Symbol]]` links, `@rto:` annotations naming an unknown or superseded ADR, \
malformed ADRs, duplicate `adr-id`s. READ `gate` FIRST: `pass`, `fail`, or `not-run`. \
`not-run` is a real outcome — the check refuses rather than answer about a tree that is \
nobody's — and carries NO `report` at all, so if you are looking for `violations` and \
there is no `report`, nothing was checked and you must say so rather than report a clean \
repository; `not_run_reason` says what to fix (usually: run `roteiro sync`). Read-only: it \
does not rebuild the graph, which is the one thing the CLI gate does that this cannot.";
pub const CONFIG_SECRETS: &str = "Inventory the SECRET-NAMED config keys in the graph: their file paths, their key names, \
and whether each value was redacted before being stored (`state` = redacted | declared | \
present). Answers \"which of this repo's config surfaces deal in credentials\" and \"did \
anything unredacted get into this graph\". THIS IS NOT A SECRET SCANNER — state the limits \
when you report it, and never imply a security guarantee. It CANNOT find a hardcoded \
credential in source code: it reads config-key nodes, so a token in a Rust or Python \
string literal produces nothing here and is invisible. It CANNOT judge whether a value is \
valid, because it never sees one — values are redacted before they reach the store. It \
CANNOT tell a real secret from a placeholder: `API_TOKEN=changeme` in a committed \
`.env.example` and a live token are the same row. And an EMPTY RESULT DOES NOT MEAN THERE \
ARE NO SECRETS — it means no config key is secret-NAMED; a credential under an innocuous \
key like `dsn` or `endpoint` never appears. If asked to scan for secrets, say plainly that \
this tool cannot do it. `limit` is 1-200 (default 50) — no unlimited setting.";
pub const CONTEXT: &str = "Fetch a node's CONTEXT BUNDLE: the node, its metadata, and its one-hop provenance-labelled \
neighbourhood, with a validity `fingerprint` that moves when the node or any neighbour \
changes. The grounding to answer “what is this and what is it wired to” from. BOUNDED, \
and it tells you when it bound something: each direction carries at most {cap} edges, and \
beyond that `truncated` is true, `outgoing.total`/`incoming.total` give the real counts, \
and `omitted` names each edge kind and how many of it are missing. Read `omitted` before \
concluding anything from an absence — a large file's missing definitions are counted \
rather than silently dropped — and use `explain` or `search` to reach what was left out.";
pub const COUPLING: &str = "Rank symbols by DIRECTED call coupling over `calls` edges: `fan_in` (how many distinct \
symbols call this one), `fan_out` (how many it calls), `instability` = \
fan_out/(fan_in+fan_out). `order`=fan_in finds what the codebase most depends on, \
`order`=fan_out the symbols that reach furthest, `total` (the default) overall coupling. \
Call edges are resolved by simple name, so a short generically-named function can absorb \
every call to that name — say so if you report a high `fan_in` on one. `limit` is 1-100 \
(default 20) — no unlimited setting.";
pub const DEBT: &str = "List intent-debt markers found in the codebase — TODO/FIXME/HACK comments, \
todo!()/unimplemented!() stubs, and deferred-work notes — grouped by category (todo, \
fixme, hack, stub, deferred), optionally restricted to some of them. Each marker links to \
its enclosing symbol or file via a `contains` edge.";
pub const DEBT_DENSITY: &str = "Rank FILES by intent-debt DENSITY — markers per 1,000 lines — rather than by raw marker \
count, which ranks the biggest file first by construction. `overall_per_kloc` is the \
repository baseline to read a file's `per_kloc` against. Use `debt` instead when the \
question is which markers exist, not where they are concentrated. Two limits to pass on \
rather than reporting a number as a finding: the denominator is FILE LENGTH — every line, \
blanks and comments included — not source lines of code, so figures run lower than an SLOC \
tool's and flatter verbose or generated files; and the markers beneath it include prose \
matches (`for now`, `deferred`, `tbd`), so a design document can rank as dense debt. This \
is a measurement, not a gate. `limit` is 1-100 (default 20) — no unlimited setting.";
pub const EXPLAIN: &str = "Explain a graph node: its record and its provenance-labelled incoming/outgoing edges. Keys \
look like `sym:<lang>:<path>#<Name>`, `file:<path>`, `adr:<id>`. A key may be \
project-qualified (`<project>::<key>`) to follow a cross-repo link into another hosted \
project (see `list_projects`).";
pub const LIST_PROJECTS: &str = "List the projects this server hosts (often just one). Pass one as `project` to the other \
tools to query it (ADR-0008). A single-project server needs no `project`.";
pub const PATH: &str = "Find a shortest path between two graph nodes, following edges in either direction. Each \
hop records the edge kind, provenance and direction. A path lives within one project: a \
project-qualified `from` (<project>::<key>) selects that project (see list_projects).";
pub const SEARCH: &str = "Search graph nodes by text — names, keys, paths, and captured content (doc comments, \
README/ADR/blueprint prose). Returns the top matches with keys and, for content-bearing \
nodes, a short `snippet` of the node's actual content to ground your answer; curated \
ADRs/blueprints and READMEs rank first, so this is the entry point for \"what is X / why\" \
questions. `limit` is 1-25 (default 10) — no unlimited setting.";
#[must_use]
pub fn for_tool(name: &str) -> Option<String> {
let raw = match name {
"check" => CHECK,
"config_secrets" => CONFIG_SECRETS,
"context" => CONTEXT,
"coupling" => COUPLING,
"debt" => DEBT,
"debt_density" => DEBT_DENSITY,
"explain" => EXPLAIN,
"list_projects" => LIST_PROJECTS,
"list_tool_classes" => LIST_TOOL_CLASSES,
"path" => PATH,
"sandbox_clear" => SANDBOX_CLEAR,
"sandbox_status" => SANDBOX_STATUS,
"search" => SEARCH,
"security_list" => SECURITY_LIST,
"security_status" => SECURITY_STATUS,
_ => return None,
};
Some(raw.replace("{cap}", &rto_graph::TOOL_CONTEXT_EDGE_CAP.to_string()))
}
#[cfg(test)]
mod tests {
use super::for_tool;
const OWNED: [&str; 15] = [
"check",
"config_secrets",
"context",
"coupling",
"debt",
"debt_density",
"explain",
"list_projects",
"list_tool_classes",
"path",
"sandbox_clear",
"sandbox_status",
"search",
"security_list",
"security_status",
];
const DESCRIPTION_BYTE_BUDGET: usize = 10_700;
#[test]
fn no_description_reaches_a_caller_with_a_placeholder_in_it() {
for name in OWNED {
let text = for_tool(name).expect("this module owns every name above");
assert!(
!text.contains('{'),
"`{name}` still carries a placeholder: {text}"
);
}
assert!(for_tool("list_kind").is_none(), "MCP-only, not owned here");
assert!(for_tool("nope").is_none());
}
#[test]
fn no_advertised_description_carries_a_run_of_spaces() {
for name in OWNED {
let text = for_tool(name).expect("owned");
let Some(at) = text.find(" ") else { continue };
panic!(
"`{name}` has a run of spaces at byte {at}. It reaches the model and \
costs bytes against `DESCRIPTION_BYTE_BUDGET`. A continued line \
carries its separating space BEFORE the backslash, never after it \
as well: …{}…",
window_around(&text, at, 60),
);
}
}
fn window_around(text: &str, at: usize, radius: usize) -> &str {
let mut from = at.saturating_sub(radius);
while !text.is_char_boundary(from) {
from -= 1;
}
let mut to = at.saturating_add(radius).min(text.len());
while !text.is_char_boundary(to) {
to += 1;
}
&text[from..to]
}
#[test]
fn a_diagnostic_window_never_splits_a_character() {
let text = "alpha — bravo — charlie — delta";
let dash = text.find('—').expect("an em dash");
for radius in 0..text.len() + 4 {
for at in [0, dash, dash + 3, text.len()] {
let got = window_around(text, at, radius);
assert!(
text.contains(got),
"window must be a real substring: {got:?}"
);
}
}
assert!(
window_around(text, dash + 3, 1).contains('—'),
"a window abutting a multi-byte character must include it whole",
);
}
#[test]
fn context_states_the_edge_cap_the_code_enforces() {
let text = for_tool("context").expect("context");
assert!(
text.contains(&format!(
"at most {} edges",
rto_graph::TOOL_CONTEXT_EDGE_CAP
)),
"the cap in the prose must be the one `bound_edges` applies: {text}"
);
}
#[test]
fn the_advertised_description_prose_stays_within_its_budget() {
let mut rows: Vec<(usize, &str)> = OWNED
.iter()
.map(|name| {
let text = for_tool(name).expect("owned");
(text.len(), *name)
})
.collect();
rows.sort_unstable_by(|a, b| b.cmp(a));
let total: usize = rows.iter().map(|(bytes, _)| bytes).sum();
assert!(
total <= DESCRIPTION_BYTE_BUDGET,
"advertised description prose is {total} bytes, over the \
{DESCRIPTION_BYTE_BUDGET}-byte budget by {}. Every byte here is \
prefilled on every tooled turn, on both the served listing and a \
local model's chat template. Either cut the sentence something else \
already upholds — the schema, a refusal in code, or the result body \
— or raise the budget in this commit and say what it bought. \
Largest first: {rows:?}",
total - DESCRIPTION_BYTE_BUDGET,
);
}
}