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