1use indexmap::IndexMap;
4use rmcp::schemars;
5
6pub(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#[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#[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#[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 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#[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#[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#[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 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#[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#[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#[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#[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#[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#[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#[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}