1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// SPDX-License-Identifier: GPL-3.0-only
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,
}
/// The onboarding entry memory — a stable global-orientation signpost (SL-201
/// D1). Single-source constant (STD-001); the `onboard` verb focuses on it.
pub(crate) const ONBOARDING_MEMORY_KEY: &str = "mem.signpost.doctrine.overview";
fn validate_focus(s: &str) -> Result<String, String> {
// Memory refs (`mem.<key>` / `mem_<uid>`): a canonical id or bare numeric can
// never start with these prefixes, so they are an unambiguous discriminator.
// Shape-check only here (pure, no disk); `run_serve` resolves key→uid.
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}"));
}
// Accept both prefixed (SL-001) and bare (1) forms.
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())?;
// A memory-ref focus resolves to its `mem_<uid>` here (the frontend addresses
// memory by uid). Resolution spans items/ AND shipped/ (the onboarding memory
// is shipped) via `collect_all` — `resolve_inspect_uid` is items-only and
// misses shipped keys. An unknown ref errors before the server binds.
// Non-memory focus (canonical id / numeric) passes through untouched.
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,
}))
}
/// Serve args for `doctrine onboard`: the map focused on the onboarding memory,
/// browser opened. Extracted so the wiring is unit-testable without disk.
fn onboard_args() -> MapServeArgs {
MapServeArgs {
port: 0,
path: None,
open: true,
focus: Some(ONBOARDING_MEMORY_KEY.to_owned()),
depth: 1,
}
}
/// `doctrine onboard` — drop a human into the onboarding graph in one step.
pub(crate) fn run_onboard() -> anyhow::Result<()> {
run_serve(None, onboard_args())
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[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() {
// A memory key is shape-accepted (resolution to uid happens in run_serve).
assert!(validate_focus("mem.signpost.doctrine.overview").is_ok());
}
#[test]
fn valid_focus_memory_uid() {
// A full mem_<32hex> uid is accepted.
assert!(validate_focus("mem_019e9a11b3797af3a8833c67acfa69bf").is_ok());
}
#[test]
fn invalid_focus_memory_ref() {
// A `mem`-prefixed but malformed ref (uppercase segment — not a valid
// key, not a uid) is rejected with a memory-ref message, not the
// canonical-id message.
let err = validate_focus("mem.Bad").unwrap_err();
assert!(err.contains("memory ref"), "unexpected message: {err}");
}
#[test]
fn run_onboard_wires_onboarding_focus() {
// Wiring assertion (no disk): the onboard verb serves the onboarding
// memory with the browser opened.
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() {
// Verify --path flag is parsed and wired into run_serve's root::find call.
// The flag replaces the outer path arg when provided.
let args = MapServeArgs {
path: Some(PathBuf::from("/tmp/test-doctrine-root")),
port: 0,
open: false,
focus: None,
depth: 1,
};
// If args.path is Some, it should be passed to root::find instead of the outer path.
// This test doesn't actually call root::find (requires disk), but verifies the
// flag is wired correctly — args.path.or(path) passes the right value.
assert!(args.path.is_some());
assert_eq!(args.path.unwrap(), PathBuf::from("/tmp/test-doctrine-root"));
// Verify the default: if args.path is None, outer path is used.
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 {
/// Start the local map explorer web server (loopback only)
Serve(MapServeArgs),
}
pub(crate) fn dispatch(cmd: MapCommand) -> anyhow::Result<()> {
match cmd {
MapCommand::Serve(args) => run_serve(None, args),
}
}