use std::path::PathBuf;
use clap::{Args, Subcommand};
use crate::catalog::scan::ScanMode;
#[derive(Args)]
pub(crate) struct MapServeArgs {
#[arg(long, default_value = "0")]
pub(crate) port: u16,
#[arg(long)]
pub(crate) path: Option<PathBuf>,
#[arg(long)]
pub(crate) open: bool,
#[arg(long, value_parser = validate_focus)]
pub(crate) focus: Option<String>,
#[arg(long, default_value = "1", value_parser = clap::value_parser!(u8).range(1..=3))]
pub(crate) depth: u8,
}
pub(crate) const ONBOARDING_MEMORY_KEY: &str = "mem.signpost.doctrine.overview";
fn validate_focus(s: &str) -> Result<String, String> {
if s.starts_with("mem.") || s.starts_with("mem_") {
return crate::memory::MemoryRef::parse(s)
.map(|_| s.to_owned())
.map_err(|e| format!("focus: invalid memory ref '{s}': {e}"));
}
if s.contains('-') {
crate::integrity::parse_canonical_ref(s)
.map(|_| s.to_owned())
.map_err(|e| {
format!("focus must be a canonical entity id (e.g. SL-001), got '{s}': {e}")
})
} else {
s.parse::<u32>().map(|_| s.to_owned()).map_err(|_e| {
format!(
"focus must be a numeric id or canonical entity id (e.g. 1 or SL-001), got '{s}'"
)
})
}
}
pub(crate) fn run_serve(path: Option<PathBuf>, args: MapServeArgs) -> anyhow::Result<()> {
let root = crate::root::find(args.path.or(path), &crate::root::default_markers())?;
let focus = match args.focus {
Some(f) if f.starts_with("mem.") || f.starts_with("mem_") => {
let mref = crate::memory::MemoryRef::parse(&f)
.map_err(|e| anyhow::anyhow!("invalid focus memory ref '{f}': {e}"))?;
let all = crate::memory::collect_all(&root)?;
Some(
crate::memory::resolve_memory_from_all(&all, &mref)?
.uid
.clone(),
)
}
other => other,
};
let catalog = crate::catalog::hydrate::scan_catalog(&root, ScanMode::default())
.map_err(|e| anyhow::anyhow!("{e}"))?;
let graph = crate::catalog::graph::CatalogGraph::from_catalog(&catalog);
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(crate::map_server::serve(crate::map_server::state::Config {
root,
graph,
port: args.port,
open: args.open,
focus,
depth: args.depth,
}))
}
fn onboard_args() -> MapServeArgs {
MapServeArgs {
port: 0,
path: None,
open: true,
focus: Some(ONBOARDING_MEMORY_KEY.to_owned()),
depth: 1,
}
}
pub(crate) fn run_onboard() -> anyhow::Result<()> {
run_serve(None, onboard_args())
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "test code")]
mod tests {
use super::*;
#[test]
fn valid_focus_sl001() {
assert!(validate_focus("SL-001").is_ok());
}
#[test]
fn invalid_focus_lowercase_prefix() {
assert!(validate_focus("sl-001").is_err());
}
#[test]
fn invalid_focus_bogus_prefix() {
assert!(validate_focus("BOGUS-001").is_err());
}
#[test]
fn invalid_focus_empty() {
assert!(validate_focus("").is_err());
}
#[test]
fn valid_focus_memory_key() {
assert!(validate_focus("mem.signpost.doctrine.overview").is_ok());
}
#[test]
fn valid_focus_memory_uid() {
assert!(validate_focus("mem_019e9a11b3797af3a8833c67acfa69bf").is_ok());
}
#[test]
fn invalid_focus_memory_ref() {
let err = validate_focus("mem.Bad").unwrap_err();
assert!(err.contains("memory ref"), "unexpected message: {err}");
}
#[test]
fn run_onboard_wires_onboarding_focus() {
let args = onboard_args();
assert_eq!(args.focus.as_deref(), Some(ONBOARDING_MEMORY_KEY));
assert!(args.open);
}
#[test]
fn map_serve_path_flag_passed_to_root_find() {
let args = MapServeArgs {
path: Some(PathBuf::from("/tmp/test-doctrine-root")),
port: 0,
open: false,
focus: None,
depth: 1,
};
assert!(args.path.is_some());
assert_eq!(args.path.unwrap(), PathBuf::from("/tmp/test-doctrine-root"));
let args_no_path = MapServeArgs {
path: None,
port: 0,
open: false,
focus: None,
depth: 1,
};
assert!(args_no_path.path.is_none());
}
}
#[derive(Subcommand)]
pub(crate) enum MapCommand {
Serve(MapServeArgs),
}
pub(crate) fn dispatch(cmd: MapCommand) -> anyhow::Result<()> {
match cmd {
MapCommand::Serve(args) => run_serve(None, args),
}
}