Skip to main content

memstead_mcp/tools/
mutation.rs

1//! Parameter structs for mutation (write) tools.
2
3use indexmap::IndexMap;
4use rmcp::schemars;
5
6/// Shared `note` description rendered identically on every mutation-tool
7/// parameter. One sentence, ≤280 chars, agent-authored — it lands in the
8/// commit body between the subject and the provenance trailers, and is
9/// what outer-repo session-bundling hooks aggregate per session.
10pub(crate) const NOTE_PARAM_DESCRIPTION: &str = "Agent-authored provenance note (≤280 chars, one sentence describing \
11     why this mutation happened). Lands in the per-mem commit body between \
12     the mechanical subject line and the provenance trailers (`Tool:`, \
13     `Actor:`, `Client:`), and is surfaced by the outer-repo Stop hook when \
14     aggregating session activity. Omit for pure-housekeeping edits; when \
15     `[mutations].require_notes = true` in workspace config a missing note \
16     adds a `NOTE_MISSING` `WarningHint` to the response (the mutation still \
17     commits).";
18
19/// Parameters for memstead_create.
20#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
21#[serde(deny_unknown_fields)]
22pub struct CreateParams {
23    #[schemars(description = "Entity title (ID is derived automatically as mem--slug(title))")]
24    pub title: String,
25    #[schemars(
26        description = "Entity type. Required. Allowed values are pinned by the target mem's schema — fetch them via `memstead_schema(name=<mem.schema_ref>)` (cached per session). Unknown types refuse with `UNKNOWN_ENTITY_TYPE`."
27    )]
28    pub entity_type: String,
29    #[schemars(description = "Mem name (directory name of the write mem)")]
30    pub mem: Option<String>,
31    #[schemars(description = "Section contents: { \"identity\": \"...\", \"purpose\": \"...\" }")]
32    pub sections: Option<IndexMap<String, String>>,
33    #[schemars(description = "Metadata overrides: { \"level\": \"M1\", \"tags\": \"a, b\" }")]
34    pub metadata: Option<IndexMap<String, String>>,
35    #[schemars(
36        description = "Initial relationships, wired in the same call that creates the entity. An entry is literally `{ \"to\": \"<mem>--<slug>\", \"type\": \"REL_TYPE\", \"description\": \"…\" }` — `description` optional, and there is no `from`: the entity being created is the source. The sibling surfaces spell the same edge differently — `memstead_relate` entries carry `{from, to, type, remove?, description?}`, and the CLI takes `--relation TYPE:target-id` — so read the shape here rather than carrying one over. An unresolved `to` auto-creates a stub."
37    )]
38    pub relations: Option<Vec<RelationInput>>,
39    #[schemars(
40        description = "Optional provenance anchors to attach to the new entity — durable records tying it to the source artifacts it describes (which artifact, at which grain, under which provenance class). Written into the mem-branch anchors sidecar in the SAME commit as the entity (atomic); omitting it is byte-identical to a create without anchors. Anchor writes MERGE: later `memstead_update` calls carrying `anchors` add to this set (same `(artifact, grain, class)` triple replaces, otherwise appends) and never silently discard it — removal is explicit via `memstead_update`'s `anchors_unset`. A malformed element refuses the whole create with `INVALID_ANCHOR` (`details` carries the offending field + allowed set) and the entity is not written. Anchors do NOT participate in `_hash`."
41    )]
42    #[serde(default)]
43    pub anchors: Option<Vec<AnchorInputParam>>,
44    #[schemars(
45        description = "Validate and preview the create without executing — no disk write, no store mutation, no VCS commit, no edges added. dry_run runs the SAME validation a real call runs; it is not a softer check. On a VALID entity the response carries the prospective `id`, `file_path`, and `_hash` (bit-identical to what a real call with the same arguments would produce, EXCEPT for engine-auto-stamped timestamps: the hash covers `created_date`, which is stamped from wall-clock `now()` independently in the dry-run and the real call, so the two `_hash` values diverge whenever a second ticks between them; the hash also covers `sections`, `metadata`, and `relations`, so a dry_run that omits `relations` will not match a real call that supplies them; `_hash` does NOT cover `anchors` — the anchors sidecar persists on the mem branch under `.memstead/` and is never folded into content hashing, so attaching or refreshing anchors never changes `_hash` or invalidates a cached `expected_hash`), plus any `warnings` and any `incoming` edges that would be adopted from a pre-existing stub at this id, with `commit_sha` empty. On an INVALID entity dry_run does NOT return a warnings-list preview: it refuses with the IDENTICAL typed envelope a real call would return (`MISSING_REQUIRED_SECTION`, `UNKNOWN_SECTION`, `UNKNOWN_METADATA_FIELD`, `INVALID_ENUM_VALUE`, `REQUIRED_FIELD_UNSET`, …), carrying the same recovery `details.*` (e.g. `details.sections[]`). That typed refusal IS the pre-flight signal — read its `details` to fix coverage, then retry. So dry_run never reports a problem entity as clean: it and a real write agree on validity. Use to verify the id slug, or to pre-flight required-section / field coverage and pre-existing references before committing."
46    )]
47    pub dry_run: Option<bool>,
48    #[schemars(description = NOTE_PARAM_DESCRIPTION)]
49    pub note: Option<String>,
50    #[schemars(
51        description = "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier` (agent-trust plan 13). Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary."
52    )]
53    pub role: Option<String>,
54}
55
56/// One `anchors[]` element on `memstead_create` / `memstead_update` — a
57/// provenance record tying the entity to a source artifact. Permissive by
58/// design: every field is optional / string-typed so a malformed element
59/// (unknown class or grain, missing artifact, hash on a non-hash class,
60/// grain the medium's namespace cannot express) refuses the whole mutation
61/// with a typed `INVALID_ANCHOR` envelope carrying recovery `details` —
62/// rather than an opaque schema-deserialisation error. Converts to the
63/// engine's `AnchorInput` which validates it. Not folded into `_hash`.
64#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
65#[serde(deny_unknown_fields)]
66pub struct AnchorInputParam {
67    #[schemars(
68        description = "Artifact reference in the medium's own namespace — a repo-relative path, `path@commit`, URL, or entity id, interpreted per `grain`. Required; a missing/empty value refuses INVALID_ANCHOR."
69    )]
70    #[serde(default)]
71    pub artifact: Option<String>,
72    #[schemars(
73        description = "Granularity of the artifact reference: `span` | `file` | `tree` | `url` | `entity`. Must be expressible in the resolving medium's anchor namespace (path-shaped for span/file/tree, `url` for url, `entity` for entity) or the mutation refuses INVALID_ANCHOR. An unknown value refuses INVALID_ANCHOR."
74    )]
75    #[serde(default)]
76    pub grain: Option<String>,
77    #[schemars(
78        description = "Provenance class — the entity's epistemic standing toward the artifact: `anchored` | `derived` | `authored` | `informed-by`. `anchored`/`derived` carry hash semantics (a `hash` is permitted and participates in drift adjudication); `authored`/`informed-by` do not (supplying `hash` refuses INVALID_ANCHOR). An unknown value refuses INVALID_ANCHOR."
79    )]
80    #[serde(default)]
81    pub class: Option<String>,
82    #[schemars(
83        description = "Medium-typed pinned version this anchor was recorded against: `{ kind: \"commit\"|\"snapshot\"|\"etag\", value: \"<token>\" }`. Omit for a plain-path medium with no retrievable version."
84    )]
85    #[serde(default)]
86    pub at_version: Option<AnchorVersionParam>,
87    #[schemars(
88        description = "Content hash over the PREPARED artifact form (never raw bytes). Permitted only on hash-bearing classes (`anchored`/`derived`); supplying it on `authored`/`informed-by` refuses INVALID_ANCHOR."
89    )]
90    #[serde(default)]
91    pub hash: Option<String>,
92    #[schemars(
93        description = "Medium's declared hash stability: `stable` | `unstable` (defaults to `stable`). An unstable-source hash break resolves `recheck`, not `drifted`."
94    )]
95    #[serde(default)]
96    pub hash_stability: Option<String>,
97    #[schemars(
98        description = "For a `derived` class: the input artifact refs the entity was derived from. Empty/omitted for every other class."
99    )]
100    #[serde(default)]
101    pub derived_from: Option<Vec<String>>,
102    #[schemars(
103        description = "`hash(D)` of the binding that produced this anchor, when a binding produced it. Omit for a manually-authored anchor."
104    )]
105    #[serde(default)]
106    pub binding: Option<String>,
107    #[schemars(
108        description = "NAME of the source (as declared in the producing binding's `sources[]`) that produced this anchor — lets a discovery run be measured per entry point. Name the source you are working from whenever the binding declares more than one. Present-but-empty refuses INVALID_ANCHOR; a name the (resolvable) producing binding does not declare refuses with the declared names in `details.declared`. Omit for a manually-authored anchor."
109    )]
110    #[serde(default)]
111    pub source: Option<String>,
112}
113
114/// The medium-typed pinned version sub-object of an [`AnchorInputParam`].
115#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
116#[serde(deny_unknown_fields)]
117pub struct AnchorVersionParam {
118    #[schemars(
119        description = "Version kind: `commit` (git / path+commit) | `snapshot` (graph) | `etag` (web)."
120    )]
121    pub kind: String,
122    #[schemars(description = "The version token — commit id, graph snapshot token, or web ETag.")]
123    pub value: String,
124}
125
126impl AnchorInputParam {
127    /// Lower the permissive wire element into the engine's `AnchorInput`
128    /// (which performs the typed `INVALID_ANCHOR` validation). An
129    /// unrecognised `at_version.kind` is dropped (best-effort — version
130    /// kind is not part of the anchor-validation contract this cut fires).
131    pub(crate) fn into_engine(self) -> memstead_base::anchor::AnchorInput {
132        use memstead_base::anchor::AnchorVersion;
133        let at_version = self.at_version.and_then(|v| match v.kind.as_str() {
134            "commit" => Some(AnchorVersion::Commit(v.value)),
135            "snapshot" => Some(AnchorVersion::Snapshot(v.value)),
136            "etag" => Some(AnchorVersion::Etag(v.value)),
137            _ => None,
138        });
139        memstead_base::anchor::AnchorInput {
140            artifact: self.artifact,
141            grain: self.grain,
142            source: self.source,
143            class: self.class,
144            at_version,
145            hash: self.hash,
146            hash_stability: self.hash_stability,
147            derived_from: self.derived_from,
148            binding: self.binding,
149        }
150    }
151}
152
153/// A relationship input for create/batch tools.
154#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct RelationInput {
157    #[schemars(description = "Full target entity ID")]
158    pub to: String,
159    #[schemars(
160        description = "Relationship type. Canonical form is UPPER_SNAKE_CASE (USES, PART_OF, DEPENDS_ON) and is what the engine stores; case-insensitive inputs (`uses`, `Part_Of`) are accepted and echoed back in the response as their canonical form. The JSON Schema `pattern` advertises `^[A-Za-z][A-Za-z_]*$` for client-side validators; the engine enforces the same character set independently — characters outside it return `INVALID_REL_TYPE` at the engine boundary regardless of whether the client pre-filters.",
161        regex(pattern = r"^[A-Za-z][A-Za-z_]*$")
162    )]
163    pub r#type: String,
164    #[schemars(
165        description = "Optional per-edge description text. Validated against the rel-type's `per_edge_description` posture in the pinned schema: `forbidden` (default) rejects a non-empty description with `DESCRIPTION_NOT_PERMITTED`; `required` rejects its absence with `MISSING_REQUIRED_DESCRIPTION`; `optional` accepts both. Empty / whitespace-only strings normalise to absent before validation. Surfaces on `memstead_entity` and round-trips through the `## Relationships` markdown via the canonical em-dash delimiter (` — `)."
166    )]
167    #[serde(default)]
168    pub description: Option<String>,
169}
170
171/// Parameters for memstead_update.
172#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
173#[serde(deny_unknown_fields)]
174pub struct UpdateParams {
175    #[schemars(description = "Full entity ID to update")]
176    pub id: String,
177    #[schemars(
178        description = "Hash from memstead_entity response (_hash field). Required — read the entity first. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash; pass dry_run=true to bypass the check as a recovery path."
179    )]
180    pub expected_hash: String,
181    #[schemars(
182        description = "Section fields to set (replaces content): { \"identity\": \"new content\" }"
183    )]
184    pub sections: Option<IndexMap<String, String>>,
185    #[schemars(description = "Section fields to append to: { \"specifies\": \"extra content\" }")]
186    pub append_sections: Option<IndexMap<String, String>>,
187    #[schemars(
188        description = "Section fields to patch (find-and-replace): { \"specifies\": { \"old\": \"...\", \"new\": \"...\" } }"
189    )]
190    pub patch_sections: Option<IndexMap<String, PatchInput>>,
191    #[schemars(description = "Metadata fields to set: { \"level\": \"M1\" }")]
192    pub metadata: Option<IndexMap<String, String>>,
193    #[schemars(
194        description = "Metadata keys to remove. Silent no-op if absent. Errors on the engine-stamped timestamp fields (created_date / last_modified) and on schema-required fields. The reserved identity triple (mem / id / type) is asymmetric by design: SET refuses (READ_ONLY_FIELD, here and on create) but UNSET is allowed — the sanctioned repair for an entity that acquired a smuggled reserved key before the write gates closed. Unsetting `type` never leaves the entity typeless (the engine re-seeds the authoritative discriminator; on a healthy entity it is a no-op). Cannot overlap with `metadata` keys — pass one or the other per key."
195    )]
196    pub metadata_unset: Option<Vec<String>>,
197    #[schemars(
198        description = "Validate and preview what would change without executing. On a valid update the response carries the unchanged on-disk hash as `_hash` plus the post-write `prospective_hash` — pass `_hash` as `expected_hash` on the follow-up real call. `dry_run` deliberately bypasses ONLY the `expected_hash` check (the returned `_hash` is the current on-disk hash, safe to reuse on the real follow-up), making it the designated recovery path for stale hashes. It does NOT relax the rest of validation: an update that a real call would refuse on section/field grounds (`UNKNOWN_SECTION`, `UNKNOWN_METADATA_FIELD`, `INVALID_ENUM_VALUE`, `REQUIRED_FIELD_UNSET`, `PATCH_OLD_NOT_FOUND`, …) refuses under dry_run with the same typed envelope and the same recovery `details.*` — that refusal is the pre-flight signal, not a clean preview. So dry_run and a real write agree on validity (modulo the intentionally-skipped hash check)."
199    )]
200    pub dry_run: Option<bool>,
201    #[schemars(
202        description = "Atomic batched relation declarations applied before the section/metadata changes land. Each `{ to, type }` is validated like a `memstead_relate` call (schema-shape, cross-mem policy, target-id grammar) and appended to the entity's relations; absent Write-mem targets are auto-stubbed identically to the relate path. The strict wiki-link/relation validator then runs against the post-mutation state with the freshly-declared relations in place — so adding a `[[target]]` body wiki-link + declaring the backing `REFERENCES` relation can land in a single `memstead_update` call (without `declare_relations`, the post-migration strict validator would refuse the body link). Each successful entry is echoed in `relations_declared` on the response with `target_was_stubbed` flagging whether the target was absent at call time. Omit for mutations that don't introduce new relations."
203    )]
204    pub declare_relations: Option<Vec<RelationInput>>,
205    #[schemars(
206        description = "Repair-shaped relation removals `[{ rel_type, target }]`, applied atomically within this update. Accepted only when the entity currently FAILS the conformance check (see memstead_health include=conformance) — on a conformant entity the call refuses with REPAIR_NOT_NEEDED and the entity is unmodified; use memstead_relate(remove=true) for everyday edge detachment. Absent pairs are silent no-ops (symmetric with metadata_unset). The strict-write post-condition is unchanged: the post-repair entity must validate or the whole update refuses with the relevant write-time code. During a schema migration every not-yet-repaired entity is non-conformant against the target, so this param works on exactly those entities with no mode flag."
207    )]
208    pub relations_unset: Option<Vec<RelationUnsetInput>>,
209    #[schemars(
210        description = "Optional provenance anchors to attach to this entity — durable records tying it to the source artifacts it describes. Anchors MERGE into the entity's existing set: an incoming anchor replaces the existing anchor with the same `(artifact, grain, class)` triple and appends otherwise — writing anchors never removes an anchor this call did not name in `anchors_unset` (an empty or omitted list leaves the stored set untouched; incremental anchoring works). Written into the mem-branch anchors sidecar in the SAME commit as the update (atomic). An update carrying only `anchors` (no section/metadata change) still commits the sidecar. A malformed element refuses the whole update with `INVALID_ANCHOR` and nothing is written. Anchors do NOT participate in `_hash`."
211    )]
212    #[serde(default)]
213    pub anchors: Option<Vec<AnchorInputParam>>,
214    #[schemars(
215        description = "Explicit anchor removals, applied BEFORE the `anchors` merge in the same mutation (mirroring `metadata_unset` / `relations_unset`) — removal is explicit, never a side effect of writing. Each entry names an `artifact` and may narrow by `grain` and/or `class`; a bare artifact removes every anchor on it. Unsetting an anchor that does not exist is a no-op, not an error. Full-replace stays expressible: unset the artifact(s) and write the new set in one call. A malformed selector (missing artifact, unknown grain/class) refuses the whole update with `INVALID_ANCHOR`."
216    )]
217    #[serde(default)]
218    pub anchors_unset: Option<Vec<AnchorUnsetParam>>,
219    #[schemars(description = NOTE_PARAM_DESCRIPTION)]
220    pub note: Option<String>,
221    #[schemars(
222        description = "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier` (agent-trust plan 13). Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary."
223    )]
224    pub role: Option<String>,
225}
226
227/// One `anchors_unset[]` entry on `memstead_update` — an explicit anchor-
228/// removal selector. Permissive like [`AnchorInputParam`]: a malformed
229/// selector refuses the whole mutation with a typed `INVALID_ANCHOR`
230/// envelope rather than an opaque schema-deserialisation error.
231#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
232#[serde(deny_unknown_fields)]
233pub struct AnchorUnsetParam {
234    #[schemars(
235        description = "Artifact reference whose anchors to remove, exactly as stored. Required; a missing/empty value refuses INVALID_ANCHOR. Bare (no grain/class) removes every anchor on the artifact."
236    )]
237    #[serde(default)]
238    pub artifact: Option<String>,
239    #[schemars(
240        description = "Optional narrowing: only remove anchors of this grain (`span` | `file` | `tree` | `url` | `entity`). An unknown value refuses INVALID_ANCHOR."
241    )]
242    #[serde(default)]
243    pub grain: Option<String>,
244    #[schemars(
245        description = "Optional narrowing: only remove anchors of this provenance class (`anchored` | `derived` | `authored` | `informed-by`). An unknown value refuses INVALID_ANCHOR."
246    )]
247    #[serde(default)]
248    pub class: Option<String>,
249}
250
251impl AnchorUnsetParam {
252    /// Lower the permissive wire element into the engine's
253    /// `AnchorUnsetInput` (which performs the typed `INVALID_ANCHOR`
254    /// validation).
255    pub(crate) fn into_engine(self) -> memstead_base::anchor::AnchorUnsetInput {
256        memstead_base::anchor::AnchorUnsetInput {
257            artifact: self.artifact,
258            grain: self.grain,
259            class: self.class,
260        }
261    }
262}
263
264/// One `relations_unset` entry — `{ rel_type, target }`.
265#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
266#[serde(deny_unknown_fields)]
267pub struct RelationUnsetInput {
268    #[schemars(
269        description = "Relationship type of the edge to remove (canonical UPPER_SNAKE_CASE; case-insensitive input accepted)"
270    )]
271    pub rel_type: String,
272    #[schemars(description = "Full target entity ID of the edge to remove")]
273    pub target: String,
274}
275
276/// Find-and-replace input.
277#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
278#[serde(deny_unknown_fields)]
279pub struct PatchInput {
280    #[schemars(description = "Exact substring to find in current content")]
281    pub old: String,
282    #[schemars(description = "Replacement (empty string = delete)")]
283    pub new: String,
284    #[schemars(
285        description = "Replace every occurrence of `old` when true; replace only the first when false or omitted. Literal match, case-sensitive."
286    )]
287    pub all: Option<bool>,
288}
289
290/// One relation operation in `memstead_relate`'s list.
291#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
292#[serde(deny_unknown_fields)]
293pub struct RelateOpInput {
294    #[schemars(description = "Full source entity ID")]
295    pub from: String,
296    #[schemars(description = "Full target entity ID")]
297    pub to: String,
298    #[schemars(
299        description = "Relationship type. Canonical form is UPPER_SNAKE_CASE (USES, PART_OF, DEPENDS_ON) and is what the engine stores; case-insensitive inputs (`uses`, `Part_Of`) are accepted and echoed back in the response as their canonical form. The JSON Schema `pattern` advertises `^[A-Za-z][A-Za-z_]*$` for client-side validators; the engine enforces the same character set independently — characters outside it return `INVALID_REL_TYPE` at the engine boundary regardless of whether the client pre-filters.",
300        regex(pattern = r"^[A-Za-z][A-Za-z_]*$")
301    )]
302    pub r#type: String,
303    #[schemars(description = "Set true to remove the relationship instead of creating it")]
304    pub remove: Option<bool>,
305    #[schemars(
306        description = "Optional per-edge description applied on add. Validated against the rel-type's `per_edge_description` posture in the pinned schema: `forbidden` (default) rejects a non-empty description with `DESCRIPTION_NOT_PERMITTED`; `required` rejects its absence with `MISSING_REQUIRED_DESCRIPTION`; `optional` accepts both. Empty / whitespace-only strings normalise to absent before validation. Ignored on the remove path."
307    )]
308    #[serde(default)]
309    pub description: Option<String>,
310}
311
312/// Parameters for memstead_relate — a list of relation operations
313/// applied atomically. The single-relation call is a list of one.
314#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
315#[serde(deny_unknown_fields)]
316pub struct RelateParams {
317    #[schemars(
318        description = "Relation operations, applied atomically in order — all-or-nothing in one commit per touched mem. Each entry is `{from, to, type, remove?, description?}` with per-entry validation identical to a single call; later entries validate against the graph state produced by earlier ones (an acyclic check sees edges added earlier in the list). A single failing entry refuses the WHOLE list and the refusal reports every failing entry."
319    )]
320    pub relations: Vec<RelateOpInput>,
321    #[schemars(description = NOTE_PARAM_DESCRIPTION)]
322    pub note: Option<String>,
323    #[schemars(
324        description = "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier` (agent-trust plan 13). Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary."
325    )]
326    pub role: Option<String>,
327    #[schemars(
328        description = "Validate and preview the relation operations without executing — no edge lands, no stub is created, no VCS commit. dry_run runs the SAME validation a real call runs (cross-mem policy, vocabulary, description posture, acyclicity, self-loop refusal); an illegal operation refuses with the IDENTICAL typed envelope a real call would return, and a legal one reports the would-be action with `_hash` set to the PROSPECTIVE post-write source hash, `commit_sha` empty (the rehearsal marker), and any would-be `AUTO_STUB_CREATED` warning for an absent target — reported, never created. The follow-up real call on an unchanged mem succeeds; like create's dry_run, its `_hash` diverges from the rehearsed one whenever a wall-clock second ticks between the calls (the auto-stamped `last_modified` enters the hash) — a timestamp shift, not drift."
329    )]
330    pub dry_run: Option<bool>,
331}
332
333/// Parameters for memstead_delete.
334#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
335#[serde(deny_unknown_fields)]
336pub struct DeleteParams {
337    #[schemars(description = "Full entity ID to delete")]
338    pub id: String,
339    #[schemars(
340        description = "Hash from memstead_entity response (_hash field). Required for real entities — read first. Mirrors memstead_update / memstead_rename. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash. Stubs carry an empty `_hash` (they have no on-disk file); pass the empty string to delete a stub — the hash check is skipped because there is nothing to compare."
341    )]
342    pub expected_hash: String,
343    #[schemars(description = NOTE_PARAM_DESCRIPTION)]
344    pub note: Option<String>,
345    #[schemars(
346        description = "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier` (agent-trust plan 13). Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary."
347    )]
348    pub role: Option<String>,
349}
350
351/// Parameters for memstead_rename.
352#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
353#[serde(deny_unknown_fields)]
354pub struct RenameParams {
355    #[schemars(description = "Full current entity ID")]
356    pub id: String,
357    #[schemars(description = "New title for the entity")]
358    pub new_title: String,
359    #[schemars(
360        description = "Hash from memstead_entity (_hash). Required. Mismatch returns code HASH_MISMATCH with details.current carrying the current on-disk hash."
361    )]
362    pub expected_hash: String,
363    #[schemars(description = NOTE_PARAM_DESCRIPTION)]
364    pub note: Option<String>,
365    #[schemars(
366        description = "The role this mutation is performed in, from the closed vocabulary `author` | `checker` | `verifier` (agent-trust plan 13). Recorded immutably alongside the mutation (commit trailer / ledger) — caller-declared but tamper-evident: bound to this operation in append-only history, it cannot be edited afterwards and identities can be cross-checked across operations. Omit to record the session default (or unspecified — legal forever, never refused, treated downstream as cannot-confirm). An unknown value refuses INVALID_ROLE naming the vocabulary."
367    )]
368    pub role: Option<String>,
369}
370
371/// Parameters for `memstead_check` — the check operation
372/// (agent-trust plan 14). Deliberately minimal: entity, verdict,
373/// optional method note, optional role.
374#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
375pub struct CheckParams {
376    #[schemars(description = "Full entity id (`mem--slug`) of the entity that was checked")]
377    pub entity: String,
378    #[schemars(
379        description = "The verdict, from the closed vocabulary `ok` | `failed`. Nuance goes in `method` or in process-mem entities — an unknown value refuses `INVALID_VERDICT` naming the vocabulary."
380    )]
381    pub verdict: String,
382    #[schemars(
383        description = "Optional free-text method note — how the check was performed (e.g. \"diffed against source spec\")."
384    )]
385    pub method: Option<String>,
386    #[schemars(
387        description = "The role this check is performed in, from the closed vocabulary `author` | `checker` | `verifier`. Recorded immutably on the check record — same trust model as mutation roles: caller-declared but tamper-evident. Omit to record the session default (or unspecified — legal, but an unspecified-role check cannot confirm independence downstream)."
388    )]
389    pub role: Option<String>,
390}