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