memstead_cli/commands/
mod.rs1pub mod anchors;
2pub mod changes;
3pub mod context;
4pub mod create;
5
6pub const SLUG_DERIVATION_HELP: &str = "\
12Slug derivation:
13 The entity slug derives from the title in five steps:
14 1. NFC-normalize (combining sequences fold to precomposed form);
15 2. Unicode case-fold to lowercase;
16 3. rewrite each whitespace character to '-';
17 4. drop every character that is not Unicode alphanumeric and not '-';
18 5. collapse hyphen runs, trim leading/trailing hyphens.
19
20 The mutation entry refuses titles where step 4 would drop any
21 character, or where the pipeline output is empty (whitespace- or
22 hyphen-only input). Errors carry a `proposed_slug` recovery hint
23 so a sanitised retry is mechanical.
24
25 The title body is stored as-sent (byte-form preserved); slug bytes
26 derive from the NFC-normalised form. An NFD-spelled title therefore
27 produces an NFC-spelled slug — the two byte forms are semantically
28 equivalent and compare equal under NFC normalization.
29
30 Pre-gate entities (created before this stricter rule landed) remain
31 readable. The gate runs at mutation entry only — it does not
32 retroactively reject entities loaded from disk.";
33
34pub const SECTION_BYTES_VERBATIM_HELP: &str = "\
40Section / append / patch flag values:
41 `--section KEY=VALUE`, `--append KEY=VALUE`, and `--patch KEY=OLD=>NEW`
42 store the right-hand side as bytes verbatim. The CLI does NOT
43 interpret backslash escapes — `--section purpose=\"line1\\nline2\"`
44 writes the literal two-character sequence `\\n` into the section
45 body, not a newline.
46
47 For multi-line section content, use `--from <FILE>` where FILE is a
48 JSON payload matching the MCP `memstead_create` / `memstead_update` shape.
49 The JSON parser de-escapes `\\n`, `\\t`, etc. before the engine
50 sees the value, so a JSON-quoted `\"line1\\nline2\"` round-trips as
51 two lines on disk.";
52
53pub const CREATE_AFTER_LONG_HELP: &str = concat!(
58 "Section / append / patch flag values:\n",
59 " `--section KEY=VALUE`, `--append KEY=VALUE`, and `--patch KEY=OLD=>NEW`\n",
60 " store the right-hand side as bytes verbatim. The CLI does NOT\n",
61 " interpret backslash escapes — `--section purpose=\"line1\\nline2\"`\n",
62 " writes the literal two-character sequence `\\n` into the section\n",
63 " body, not a newline.\n",
64 "\n",
65 " For multi-line section content, use `--from <FILE>` where FILE is a\n",
66 " JSON payload matching the MCP `memstead_create` / `memstead_update` shape.\n",
67 " The JSON parser de-escapes `\\n`, `\\t`, etc. before the engine\n",
68 " sees the value, so a JSON-quoted `\"line1\\nline2\"` round-trips as\n",
69 " two lines on disk.\n",
70 "\n",
71 "Slug derivation:\n",
72 " The entity slug derives from the title in five steps:\n",
73 " 1. NFC-normalize (combining sequences fold to precomposed form);\n",
74 " 2. Unicode case-fold to lowercase;\n",
75 " 3. rewrite each whitespace character to '-';\n",
76 " 4. drop every character that is not Unicode alphanumeric and not '-';\n",
77 " 5. collapse hyphen runs, trim leading/trailing hyphens.\n",
78 "\n",
79 " The mutation entry refuses titles where step 4 would drop any\n",
80 " character, or where the pipeline output is empty (whitespace- or\n",
81 " hyphen-only input). Errors carry a `proposed_slug` recovery hint\n",
82 " so a sanitised retry is mechanical.\n",
83 "\n",
84 " The title body is stored as-sent (byte-form preserved); slug bytes\n",
85 " derive from the NFC-normalised form. An NFD-spelled title therefore\n",
86 " produces an NFC-spelled slug — the two byte forms are semantically\n",
87 " equivalent and compare equal under NFC normalization.\n",
88 "\n",
89 " Pre-gate entities (created before this stricter rule landed) remain\n",
90 " readable. The gate runs at mutation entry only — it does not\n",
91 " retroactively reject entities loaded from disk.",
92);
93
94pub const FILTER_HELP: &str = "\
98Filter surface:
99 Named-flag shortcuts (frozen — no new ones are added; use --filter
100 for any other schema-declared filterable field):
101 --type <T> Filter by entity_type (engine first-class axis).
102 --level <L> Filter by level (e.g. M0, M1).
103 --status <S> Filter by status (e.g. active, closed).
104 --edge-type <E> Filter by edge type (engine first-class axis).
105
106 Generic equality filter:
107 --filter KEY=VALUE Filter by any schema-declared `filterable: equality`
108 field. Repeatable. Examples:
109 --filter tags=auth
110 --filter scope=subsystem
111 --filter confidence=high
112 --filter tags=auth --filter level=M0
113 Unknown keys are silently dropped by the engine and surface as a
114 warning. Named-flag shortcuts and `--filter` populate the same
115 underlying filter map; if both set the same key, `--filter` wins
116 (declared last in the iteration order).";
117
118pub fn parse_filter_arg(raw: &str) -> Result<(String, String), crate::CliError> {
122 let (key, value) = raw.split_once('=').ok_or_else(|| {
123 crate::CliError::new(
124 crate::output::ExitKind::Validation,
125 "INVALID_INPUT",
126 format!("--filter expects KEY=VALUE, got `{raw}`"),
127 )
128 })?;
129 if key.is_empty() {
130 return Err(crate::CliError::new(
131 crate::output::ExitKind::Validation,
132 "INVALID_INPUT",
133 format!("--filter key must be non-empty: `{raw}`"),
134 ));
135 }
136 Ok((key.to_string(), value.to_string()))
137}
138
139pub fn render_type_guidance_block(
146 type_guidance: &std::collections::BTreeMap<String, Vec<String>>,
147) -> String {
148 if type_guidance.is_empty() {
149 return String::new();
150 }
151 let mut out = String::new();
152 for (entity_type, rules) in type_guidance {
153 if rules.is_empty() {
154 continue;
155 }
156 out.push_str(&format!("\n>\n> Type-level guidance for `{entity_type}`:",));
157 for rule in rules {
158 out.push_str(&format!("\n> - {rule}"));
159 }
160 }
161 out
162}
163
164pub fn merge_mem_changed_json(
171 body: &mut serde_json::Value,
172 notices: &[memstead_base::ops::MemChangedNotice],
173) {
174 if notices.is_empty() {
175 return;
176 }
177 if let Some(obj) = body.as_object_mut() {
178 obj.insert(
179 "mem_changed".to_string(),
180 serde_json::to_value(notices).unwrap_or(serde_json::Value::Null),
181 );
182 }
183}
184
185pub fn render_mem_changed_block(notices: &[memstead_base::ops::MemChangedNotice]) -> String {
191 use memstead_base::ops::NoticeChanges;
192 if notices.is_empty() {
193 return String::new();
194 }
195 let mut out = String::from("\n\n## Mem changed under you");
196 for n in notices {
197 out.push_str(&format!(
198 "\n\n- `{}` advanced `{}` → `{}`",
199 n.mem, n.from_head, n.to_head
200 ));
201 match &n.changes {
202 NoticeChanges::Detailed { entries } => {
203 for e in entries {
204 out.push_str(&format!("\n - {} `{}`", e.action(), e.primary_id()));
205 }
206 }
207 NoticeChanges::Ids { entries } => {
208 out.push_str(&format!(
209 "\n - {} changed ids — `memstead changes-since --since {}` for the full delta",
210 entries.len(),
211 n.from_head
212 ));
213 }
214 NoticeChanges::Counts { self_inform, .. } => {
215 out.push_str(&format!("\n - mass change — {self_inform}"));
216 }
217 }
218 }
219 out
220}
221
222pub mod admin;
223pub mod delete;
224pub mod domain;
225pub mod entity;
226pub mod export;
227pub mod health;
228pub mod init;
229pub mod link;
230pub mod list;
231pub mod login;
232pub mod logout;
233pub mod overview;
234pub mod projection;
235pub mod publish;
236pub mod quickstart;
237pub mod relate;
238pub mod relations;
239pub mod reload;
240pub mod rename;
241pub mod schema;
242pub mod search;
243pub mod status;
244pub mod type_cmd;
245pub mod unpublish;
246pub mod update;
247
248#[cfg(feature = "mem-repo")]
252pub mod batch_update;
253#[cfg(feature = "mem-repo")]
254pub mod branch_reset;
255#[cfg(feature = "mem-repo")]
256pub mod install;
257#[cfg(feature = "mem-repo")]
258pub mod mem;
259#[cfg(feature = "mem-repo")]
260pub mod mem_repo;
261#[cfg(feature = "mem-repo")]
262pub mod recover;
263#[cfg(feature = "mem-repo")]
264pub mod transport;
265#[cfg(feature = "mem-repo")]
266pub mod workspace;