1use memstead_base::ops::Query;
4use rmcp::schemars;
5
6#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
8#[serde(deny_unknown_fields)]
9pub struct EntityParams {
10 #[schemars(
11 description = "Full entity ID as returned by search/list (e.g. \"specs--my-entity\")"
12 )]
13 pub id: String,
14 #[schemars(
15 description = "Append a `## Relations` section with typed edges grouped by direction."
16 )]
17 pub include_relations: Option<bool>,
18 #[schemars(
19 description = "Append a `## Community Context` section — the entity's cluster summary, members, and bridges to other clusters."
20 )]
21 pub include_context: Option<bool>,
22 #[schemars(
23 description = "Only return these sections (default: all). Use to read specific parts of large entities."
24 )]
25 pub sections: Option<Vec<String>>,
26 #[schemars(
27 description = "Max tokens for the rendered-markdown text channel only. If the text exceeds this, returns chunk 1 of N with _truncated in its frontmatter; use the chunk param to read subsequent chunks. The structured_content envelope is never chunked or truncated by this — it always ships whole (size it ahead via its _tokens field)."
28 )]
29 pub token_budget: Option<usize>,
30 #[schemars(
31 description = "Which chunk to read (1-based). Only needed for entities that exceed the token budget."
32 )]
33 pub chunk: Option<usize>,
34 #[schemars(
35 description = "Append a `mutation_provenance` block to structured_content: `created_by` and `last_modified_by`, each with actor, client, the caller-declared `role` (or `unspecified` — absence served as cannot-confirm, never as a real role), timestamp, and the backend reference. Derived from the append-only mutation record (commit trailers / ledger), which no verb can edit after the fact — the tamper-evident half of the role trust model. When the recorded story does not start at the entity's creation, `created_by` is absent and `story_truncated` is true (stated, never fabricated). Default false: responses are byte-unchanged without the flag."
36 )]
37 pub include_provenance: Option<bool>,
38}
39
40#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
42#[serde(deny_unknown_fields)]
43pub struct SearchParams {
44 #[schemars(
45 description = "Structured flat query. Fields: `any: [terms]` (OR, ranks entities matching more terms higher — no explicit AND needed), `not: [terms]` (exclusion), `phrase: \"exact adjacency\"`, `field: \"title\"|section-key` (narrow all three). Omit (or pass `{}`) to use search as a pure structural/metadata filter — hits come back in title-ascending order. No stemming: include morphological variants explicitly (run, running, runs)."
46 )]
47 pub query: Option<Query>,
48 #[schemars(description = "Only entities in this mem")]
49 pub mem: Option<String>,
50 #[schemars(description = "Only entities of this type (e.g. \"spec\", \"memo\")")]
51 pub entity_type: Option<String>,
52 #[schemars(
53 description = "Relationship types to follow from primary hits to pull in graph-proximal neighbours (e.g. [\"REALIZES\", \"REFERENCES\"]). Expanded hits carry `expansion: { of, via_edge, depth }` and a decayed score (0.5^depth). `by_expansion` facet shows the primary/expanded composition."
54 )]
55 pub expand_via: Option<Vec<String>>,
56 #[schemars(description = "Max hops to traverse via `expand_via` (default: 1).")]
57 pub expand_depth: Option<usize>,
58 #[schemars(
59 description = "Full entity ID — only return entities within depth hops (BFS, undirected). Results are ranked by proximity: nearer hops first, then a typed (dependency) link to the anchor before a co-mention at the same hop. A neighbourhood larger than the cap is bounded to its nearest members with a `NEIGHBOURHOOD_CAPPED` warning (`kept`/`total`)."
60 )]
61 pub related_to: Option<String>,
62 #[schemars(description = "Max hops from related_to (default: 1, ignored without related_to)")]
63 pub depth: Option<usize>,
64 #[schemars(
65 description = "Traversal direction for `related_to` and `expand_via`, applied at EVERY hop: \"out\" follows edges pointing away from the seed (what does this rest on), \"in\" follows edges pointing at it (what rests on this), \"both\" (default) is the historical undirected walk. Depth > 1 is a pure transitive closure in the chosen direction — never a mixed walk. Expanded hits report the reaching edge's direction as `expansion.via_direction`."
66 )]
67 pub direction: Option<memstead_base::graph::query::TraversalDirection>,
68 #[schemars(description = "Only entities having this edge type (e.g. IMPLEMENTS, USES)")]
69 pub edge_type: Option<String>,
70 #[schemars(description = "Max results to return (default: all, max: 200)")]
71 pub limit: Option<usize>,
72 #[schemars(description = "Skip first N results for pagination. Use with limit.")]
73 pub offset: Option<usize>,
74 #[schemars(
75 description = "Equality filters on schema-declared filterable fields, keyed by field name (e.g. `{\"level\": \"M0\", \"status\": \"active\", \"tags\": \"auth\", \"scope\": \"subsystem\"}`). Every field with `filterable: equality` in the type's schema is reachable here. One typed warning per outcome, branch on `code`: `FILTER_TYPE_SCOPED` (a *filterable* key declared only on other types — applied with strict type-narrowing), `FIELD_NOT_FILTERABLE` (declared but not filterable on any reachable type — ignored in both the scoped and unscoped case, result unfiltered not emptied), `UNKNOWN_FILTER_KEY` (no schema declares it — ignored), `INVALID_ENUM_VALUE` (a value outside the field's `enum_values` — the filter applies but matches nothing, so a 0-hit result isn't a true no-match; `details.allowed` lists the values). The per-field `level`/`status`/`confidence` parameters are retired — agents declare any filterable field uniformly through this map. Use `entity_type` (typed parameter) and `edge_type` (typed parameter) for the engine's first-class graph axes, not for metadata filters."
76 )]
77 pub filters: Option<std::collections::HashMap<String, String>>,
78 #[schemars(
79 description = "Range filters on schema-declared range-filterable fields, keyed by `min_<field>` / `max_<field>` (numeric) or `<field>_before` / `<field>_after` (date). Example: `{\"created_date_after\": \"2026-01-01\", \"max_score\": \"5\"}`. Every field with `filterable: range` in the type's schema is reachable here. Composable with `filters` (equality). One typed warning per outcome, branch on `code`: `RANGE_FILTER_KEY_MALFORMED` (key lacks a `min_`/`max_`/`*_before`/`*_after` shape), `RANGE_FILTER_TYPE_SCOPED` (a *range-filterable* field declared only on other types — applied with strict type-narrowing), `UNKNOWN_RANGE_FILTER_FIELD` (derived field name not declared on any reachable schema — ignored), `FIELD_NOT_RANGE_FILTERABLE` (field declared but not `filterable: range` on any reachable type — ignored in both the scoped and unscoped case, result unfiltered not emptied)."
80 )]
81 pub range_filters: Option<std::collections::HashMap<String, String>>,
82 #[schemars(
83 description = "Filter by stub status. Omit (default) = both stubs and real entities. `true` = stubs only. `false` = real entities only."
84 )]
85 pub stub: Option<bool>,
86 #[schemars(
87 description = "Token budget bounding the returned hit payload (default: 12000). A page whose hits exceed it is greedily trimmed to the highest-ranked hits that fit (at least one always returns) and a `SEARCH_RESULTS_TRUNCATED` warning carries `kept`/`budget`; `_total` still reflects the full match count, so page the remainder with `offset` or narrow the query. Raise it to pull more hits in one call when the agent can afford the tokens. Independent of `limit`, which caps the count before the budget trims by size."
88 )]
89 pub token_budget: Option<usize>,
90}
91
92#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
94#[serde(deny_unknown_fields)]
95pub struct OverviewParams {
96 #[schemars(
97 description = "Re-run community detection before returning overview (default: false). Detection is workspace-global: `rebuild` recomputes the Louvain partition over the *whole* workspace graph — it never scopes to `mem`, even when `mem` is also passed."
98 )]
99 pub rebuild: Option<bool>,
100 #[schemars(
101 description = "Which chunk to read (1-based). Only needed if overview exceeds the MCP response cap."
102 )]
103 pub chunk: Option<usize>,
104 #[schemars(
105 description = "Restrict `mems[]` and `schemas[]` to any single visible mem — read-only mounts included. `used_by` inside each schema still lists all mems sharing it. Community scope: `mem` filters which clusters are *reported* (and makes `community_bridges` source-in-mem only) — it does NOT re-run detection per mem. Detection is always workspace-global and cluster ids stay the global-pass ids; passing `mem` never renumbers or re-scopes the partition. Because detection is global and disconnected / sparsely-connected nodes collapse into a single catch-all rather than forming their own cluster, a small or isolated mem-local subgraph may surface as no cluster at all under a `mem` filter."
106 )]
107 pub mem: Option<String>,
108 #[schemars(
109 description = "Opt into heavy content. Allowed keys: \"community_members\" (entity lists per cluster), \"community_bridges\" (inter-cluster edge aggregation with up to 3 sample edges per pair), \"mem_distribution\" (per-mem type_distribution), \"dangling_links\" (renders a `## Dangling Links` section listing each unresolved body wiki-link as `source → target (in section)`; richer aggregation tracked in #12/#13). `include` keys are always shipped regardless of the token budget — use it to force content you need. Unknown keys emit a typed `warnings` entry. Schema bodies are not in this set — call memstead_schema(name=...) for the full per-type catalogue."
110 )]
111 pub include: Option<Vec<String>>,
112 #[schemars(
113 description = "Target token budget for heavy content only (`community_members`, `community_bridges`, `mem_distribution`, `dangling_links`). Default: 8000. Hard-required content (mem roster, schema refs with relationship vocabulary, community titles, workspace policy) always ships in addition — total response size will exceed this budget. When hard-required content alone exceeds the budget, `overview_mode=\"overbudget\"` signals the agent to raise the budget or scope via `mem`. Heavy content not in `include` is greedy-filled until the budget is exhausted; anything left over is advertised in `hints[]` with `estimated_tokens`. `include` keys bypass the budget. Budgets below ~10 tokens are safe but unproductive — the structured envelope still arrives (`overview_mode=\"overbudget\"`) but no useful chunking happens and the full body ships as one chunk."
114 )]
115 pub token_budget: Option<usize>,
116}
117
118#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
121#[serde(deny_unknown_fields)]
122pub struct SchemaParams {
123 #[schemars(
124 description = "Schema name as listed in memstead_overview's `## Schemas` section (e.g. \"default\" or \"default@1.0.0\"). Schemas are workspace-globally unique by name; the workspace registry resolves a bare name to the pinned version. Mutually exclusive with `mem`."
125 )]
126 pub name: Option<String>,
127 #[schemars(
128 description = "Mem name as listed in memstead_overview's `## Mems` section. The engine resolves the mem's pinned `schema_ref` from the workspace's mount roster and proceeds identically to the `name`-driven path. Mutually exclusive with `name`. Returns `UNKNOWN_MEM` when the mem is not mounted."
129 )]
130 pub mem: Option<String>,
131 #[schemars(
132 description = "Verbosity of the schema body. `\"lite\"` (default, absent) returns a cheap cold-start skeleton: entity-type names with their section keys (and `required` markers) and metadata-field shapes (name, `required`, `enum`, `default`), relationship-type names with their `allowed_sources`/`allowed_targets`, `manual_authoring`, `acyclic`, and `per_edge_description` — plus the top-level `alias_target_rel_type` pointer — with the long-form prose dropped. The lite skeleton carries every flag needed to author a legal write. `\"full\"` returns the complete payload — every description, `when_to_use`, write-rule, and writing-guidance string; escalate to full for the human-readable guidance before substantial authoring. Heavy arrays ship under distinct keys per mode (`types`/`relationships` vs. `types_summary`/`relationships_summary`). Any value other than `\"full\"`/`\"lite\"` returns `INVALID_INPUT` naming the bad value."
133 )]
134 pub verbosity: Option<String>,
135}