1use crate::cli::CliOutput;
29use crate::db;
30use crate::mcp::{
31 handle_namespace_clear_standard, handle_namespace_get_standard, handle_namespace_set_standard,
32};
33use crate::models::field_names;
34use anyhow::{Context, Result};
35use clap::{Args, Subcommand};
36use serde_json::{Value, json};
37use std::path::Path;
38
39#[derive(Args)]
40pub struct NamespaceArgs {
41 #[command(subcommand)]
42 pub action: NamespaceAction,
43}
44
45#[derive(Subcommand)]
46pub enum NamespaceAction {
47 SetStandard {
59 #[arg(long)]
61 namespace: String,
62 #[arg(long)]
65 id: String,
66 #[arg(long)]
69 parent: Option<String>,
70 #[arg(long)]
77 governance: Option<String>,
78 },
79 GetStandard {
83 #[arg(long)]
84 namespace: String,
85 #[arg(long)]
88 inherit: bool,
89 },
90 ClearStandard {
94 #[arg(long)]
95 namespace: String,
96 },
97 BatmanPolicy {
102 #[arg(long, default_value_t = 512)]
105 atomise_threshold: u32,
106 #[arg(long, default_value_t = 256)]
108 atom_max_tokens: u32,
109 #[arg(long, default_value_t = 3)]
111 max_reflection_depth: u32,
112 #[arg(long, default_value = "regex_then_llm")]
114 classify_mode: String,
115 },
116}
117
118pub fn run(
119 db_path: &Path,
120 args: NamespaceArgs,
121 json_out: bool,
122 out: &mut CliOutput<'_>,
123) -> Result<()> {
124 match args.action {
125 NamespaceAction::SetStandard {
126 namespace,
127 id,
128 parent,
129 governance,
130 } => set_standard(
131 db_path,
132 &namespace,
133 &id,
134 parent.as_deref(),
135 governance.as_deref(),
136 json_out,
137 out,
138 ),
139 NamespaceAction::GetStandard { namespace, inherit } => {
140 get_standard(db_path, &namespace, inherit, json_out, out)
141 }
142 NamespaceAction::ClearStandard { namespace } => {
143 clear_standard(db_path, &namespace, json_out, out)
144 }
145 NamespaceAction::BatmanPolicy {
146 atomise_threshold,
147 atom_max_tokens,
148 max_reflection_depth,
149 classify_mode,
150 } => batman_policy(
151 atomise_threshold,
152 atom_max_tokens,
153 max_reflection_depth,
154 &classify_mode,
155 out,
156 ),
157 }
158}
159
160fn set_standard(
161 db_path: &Path,
162 namespace: &str,
163 id: &str,
164 parent: Option<&str>,
165 governance: Option<&str>,
166 json_out: bool,
167 out: &mut CliOutput<'_>,
168) -> Result<()> {
169 let conn = db::open(db_path)?;
170 let mut params = json!({
171 "namespace": namespace,
172 "id": id,
173 });
174 if let Some(p) = parent {
175 params["parent"] = json!(p);
176 }
177 if let Some(g) = governance {
178 let gov_val: Value =
179 serde_json::from_str(g).context("--governance must be a valid JSON object")?;
180 params[field_names::GOVERNANCE] = gov_val;
181 }
182 let resp = handle_namespace_set_standard(&conn, ¶ms).map_err(|e| anyhow::anyhow!(e))?;
183 emit(out, json_out, &resp, |o, r| {
184 writeln!(
185 o.stdout,
186 "set standard: namespace='{}' standard_id='{}'{}",
187 r["namespace"].as_str().unwrap_or(""),
188 r[field_names::STANDARD_ID].as_str().unwrap_or(""),
189 r.get("parent")
190 .and_then(Value::as_str)
191 .map(|p| format!(" parent='{p}'"))
192 .unwrap_or_default(),
193 )?;
194 if let Some(gov) = r.get(field_names::GOVERNANCE) {
195 writeln!(
196 o.stdout,
197 "governance merged: {}",
198 serde_json::to_string_pretty(gov)?
199 )?;
200 }
201 Ok(())
202 })
203}
204
205fn get_standard(
206 db_path: &Path,
207 namespace: &str,
208 inherit: bool,
209 json_out: bool,
210 out: &mut CliOutput<'_>,
211) -> Result<()> {
212 let conn = db::open(db_path)?;
213 let params = json!({
214 "namespace": namespace,
215 "inherit": inherit,
216 });
217 let resp = handle_namespace_get_standard(&conn, ¶ms).map_err(|e| anyhow::anyhow!(e))?;
218 emit(out, json_out, &resp, |o, r| {
219 if let Some(chain) = r.get("chain").and_then(Value::as_array) {
220 writeln!(
221 o.stdout,
222 "namespace: {}",
223 r["namespace"].as_str().unwrap_or("")
224 )?;
225 writeln!(
226 o.stdout,
227 "chain: {}",
228 chain
229 .iter()
230 .filter_map(Value::as_str)
231 .collect::<Vec<_>>()
232 .join(" -> ")
233 )?;
234 if let Some(stds) = r.get("standards").and_then(Value::as_array) {
235 writeln!(o.stdout, "standards in chain:")?;
236 for s in stds {
237 writeln!(
238 o.stdout,
239 " - {}: {}",
240 s["namespace"].as_str().unwrap_or(""),
241 s[field_names::STANDARD_ID].as_str().unwrap_or("null")
242 )?;
243 }
244 }
245 } else if r.get(field_names::STANDARD_ID).map_or(true, Value::is_null) {
246 writeln!(o.stdout, "namespace '{}' has no standard set", namespace)?;
247 } else {
248 writeln!(
249 o.stdout,
250 "namespace: {}\nstandard_id: {}\ntitle: {}",
251 r["namespace"].as_str().unwrap_or(""),
252 r[field_names::STANDARD_ID].as_str().unwrap_or(""),
253 r["title"].as_str().unwrap_or(""),
254 )?;
255 if let Some(gov) = r.get(field_names::GOVERNANCE) {
256 writeln!(
257 o.stdout,
258 "governance:\n{}",
259 serde_json::to_string_pretty(gov)?
260 )?;
261 }
262 }
263 Ok(())
264 })
265}
266
267fn clear_standard(
268 db_path: &Path,
269 namespace: &str,
270 json_out: bool,
271 out: &mut CliOutput<'_>,
272) -> Result<()> {
273 let conn = db::open(db_path)?;
274 let params = json!({ "namespace": namespace });
275 let resp = handle_namespace_clear_standard(&conn, ¶ms).map_err(|e| anyhow::anyhow!(e))?;
276 emit(out, json_out, &resp, |o, r| {
277 writeln!(
278 o.stdout,
279 "{} standard pointer for namespace '{}'",
280 if r["cleared"].as_bool().unwrap_or(false) {
281 "cleared"
282 } else {
283 "no-op (no standard set)"
284 },
285 r["namespace"].as_str().unwrap_or(namespace),
286 )?;
287 Ok(())
288 })
289}
290
291fn batman_policy(
292 atomise_threshold: u32,
293 atom_max_tokens: u32,
294 max_reflection_depth: u32,
295 classify_mode: &str,
296 out: &mut CliOutput<'_>,
297) -> Result<()> {
298 let policy = json!({
299 "auto_atomise": true,
300 "auto_atomise_mode": crate::models::namespace::AUTO_ATOMISE_SYNCHRONOUS,
301 "auto_atomise_threshold_cl100k": atomise_threshold,
302 "auto_atomise_max_atom_tokens": atom_max_tokens,
303 "auto_classify_kind": classify_mode,
304 "max_reflection_depth": max_reflection_depth,
305 "write": "owner",
306 "promote": "any",
307 "delete": "owner",
308 "approver": "human",
309 "inherit": true,
310 });
311 writeln!(out.stdout, "{}", serde_json::to_string_pretty(&policy)?)?;
312 Ok(())
313}
314
315fn emit<F>(out: &mut CliOutput<'_>, json_out: bool, resp: &Value, human: F) -> Result<()>
316where
317 F: FnOnce(&mut CliOutput<'_>, &Value) -> Result<()>,
318{
319 if json_out {
320 writeln!(out.stdout, "{}", serde_json::to_string_pretty(resp)?)?;
321 } else {
322 human(out, resp)?;
323 }
324 Ok(())
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::cli::test_utils::{TestEnv, seed_memory};
331
332 fn run_action(env: &mut TestEnv, action: NamespaceAction, json_out: bool) -> Result<()> {
333 let db = env.db_path.clone();
334 let mut out = env.output();
335 run(&db, NamespaceArgs { action }, json_out, &mut out)
336 }
337
338 fn seed_standard(env: &TestEnv, ns: &str, title: &str) -> String {
341 seed_memory(&env.db_path, ns, title, "standard body")
342 }
343
344 #[test]
345 fn set_standard_text_output_with_governance() {
346 let mut env = TestEnv::fresh();
347 let id = seed_standard(&env, "team/alpha", "alpha-standard");
348 run_action(
349 &mut env,
350 NamespaceAction::SetStandard {
351 namespace: "team/alpha".into(),
352 id,
353 parent: None,
354 governance: Some(r#"{"write":"owner"}"#.into()),
355 },
356 false,
357 )
358 .expect("set ok");
359 let stdout = env.stdout_str();
360 assert!(stdout.contains("set standard:"), "got: {stdout}");
361 assert!(stdout.contains("governance merged:"), "got: {stdout}");
362 }
363
364 #[test]
365 fn get_standard_text_no_standard() {
366 let mut env = TestEnv::fresh();
367 run_action(
368 &mut env,
369 NamespaceAction::GetStandard {
370 namespace: "empty/ns".into(),
371 inherit: false,
372 },
373 false,
374 )
375 .expect("get ok");
376 assert!(env.stdout_str().contains("has no standard set"));
377 }
378
379 #[test]
380 fn get_standard_text_with_set_standard() {
381 let mut env = TestEnv::fresh();
382 let id = seed_standard(&env, "team/beta", "beta-standard");
383 run_action(
384 &mut env,
385 NamespaceAction::SetStandard {
386 namespace: "team/beta".into(),
387 id,
388 parent: None,
389 governance: Some(r#"{"write":"any"}"#.into()),
390 },
391 false,
392 )
393 .expect("set ok");
394 env.stdout.clear();
395 run_action(
396 &mut env,
397 NamespaceAction::GetStandard {
398 namespace: "team/beta".into(),
399 inherit: false,
400 },
401 false,
402 )
403 .expect("get ok");
404 let stdout = env.stdout_str();
405 assert!(stdout.contains("standard_id:"), "got: {stdout}");
406 assert!(stdout.contains("governance:"), "got: {stdout}");
407 }
408
409 #[test]
410 fn get_standard_text_inherit_chain() {
411 let mut env = TestEnv::fresh();
412 let id = seed_standard(&env, "team/gamma", "gamma-standard");
413 run_action(
414 &mut env,
415 NamespaceAction::SetStandard {
416 namespace: "team/gamma".into(),
417 id,
418 parent: None,
419 governance: None,
420 },
421 false,
422 )
423 .expect("set ok");
424 env.stdout.clear();
425 run_action(
426 &mut env,
427 NamespaceAction::GetStandard {
428 namespace: "team/gamma".into(),
429 inherit: true,
430 },
431 false,
432 )
433 .expect("get ok");
434 let stdout = env.stdout_str();
435 assert!(stdout.contains("chain:"), "got: {stdout}");
436 }
437
438 #[test]
439 fn clear_standard_text_no_op_then_cleared() {
440 let mut env = TestEnv::fresh();
441 run_action(
443 &mut env,
444 NamespaceAction::ClearStandard {
445 namespace: "team/delta".into(),
446 },
447 false,
448 )
449 .expect("clear ok");
450 assert!(env.stdout_str().contains("no-op"));
451
452 let id = seed_standard(&env, "team/delta", "delta-standard");
454 run_action(
455 &mut env,
456 NamespaceAction::SetStandard {
457 namespace: "team/delta".into(),
458 id,
459 parent: None,
460 governance: None,
461 },
462 false,
463 )
464 .expect("set ok");
465 env.stdout.clear();
466 run_action(
467 &mut env,
468 NamespaceAction::ClearStandard {
469 namespace: "team/delta".into(),
470 },
471 false,
472 )
473 .expect("clear ok");
474 assert!(env.stdout_str().contains("cleared standard pointer"));
475 }
476
477 #[test]
478 fn batman_policy_emits_json_policy() {
479 let mut env = TestEnv::fresh();
480 run_action(
481 &mut env,
482 NamespaceAction::BatmanPolicy {
483 atomise_threshold: 512,
484 atom_max_tokens: 256,
485 max_reflection_depth: 3,
486 classify_mode: "regex_then_llm".into(),
487 },
488 false,
489 )
490 .expect("batman ok");
491 let policy: Value = serde_json::from_str(env.stdout_str().trim()).expect("json");
492 assert_eq!(policy["auto_atomise"].as_bool(), Some(true));
493 assert_eq!(policy["max_reflection_depth"].as_u64(), Some(3));
494 assert_eq!(
495 policy["auto_classify_kind"].as_str(),
496 Some("regex_then_llm")
497 );
498 }
499
500 #[test]
501 fn set_standard_json_output() {
502 let mut env = TestEnv::fresh();
503 let id = seed_standard(&env, "team/json", "json-standard");
504 run_action(
505 &mut env,
506 NamespaceAction::SetStandard {
507 namespace: "team/json".into(),
508 id,
509 parent: None,
510 governance: None,
511 },
512 true,
513 )
514 .expect("set ok");
515 let resp: Value = serde_json::from_str(env.stdout_str().trim()).expect("json");
516 assert_eq!(resp["namespace"].as_str(), Some("team/json"));
517 }
518
519 #[test]
520 fn set_standard_invalid_governance_json_errors() {
521 let mut env = TestEnv::fresh();
522 let err = run_action(
523 &mut env,
524 NamespaceAction::SetStandard {
525 namespace: "team/bad".into(),
526 id: "std-b".into(),
527 parent: None,
528 governance: Some("{not json".into()),
529 },
530 false,
531 )
532 .expect_err("must fail");
533 assert!(err.to_string().contains("valid JSON object"), "got: {err}");
534 }
535}