diff --git a/crates/omnigraph-cli/src/cli.rs b/crates/omnigraph-cli/src/cli.rs index 53f6026..a9a5d0b 100644 --- a/crates/omnigraph-cli/src/cli.rs +++ b/crates/omnigraph-cli/src/cli.rs @@ -9,19 +9,20 @@ pub(crate) const DEFAULT_BEARER_TOKEN_ENV: &str = "OMNIGRAPH_BEARER_TOKEN"; #[command(name = "omnigraph")] #[command(about = "Omnigraph graph database CLI")] #[command(version = env!("CARGO_PKG_VERSION"), disable_version_flag = true)] -// Subcommands are listed grouped by plane (clap renders them in declaration -// order). clap can't print labeled headings between subcommand groups, so this -// legend names the planes; the grouping is the variant order in `Command`. +// Subcommands render in declaration order (clap can't print labeled headings +// between groups), so this legend names the capability each command needs — +// the user-facing vocabulary (RFC-011). `Plane` stays the internal classifier. #[command(after_help = "\ -COMMANDS BY PLANE:\n \ -Data — run against a graph, embedded or via --server (query, mutate, load, \ -branch, snapshot, export, commit, schema [plan: storage], graphs).\n \ -Storage — direct storage or local files; reject --server (init, optimize, \ -repair, cleanup, lint, queries [list: session]).\n \ -Control — manage a cluster directory via --config (cluster).\n \ -Session — no graph; local config & tooling (policy, embed, login, logout, \ -config, version).\n\ -See the 'Command planes' section of the CLI reference for which flags apply where.")] +COMMANDS BY CAPABILITY:\n \ +any — run against a graph, served (--server / --profile) or embedded (--store / a \ +URI): query, mutate, load, branch, snapshot, export, commit, schema show/apply.\n \ +served — require a server: graphs.\n \ +direct — direct storage access; reject --server (init, optimize, repair, cleanup, \ +schema plan, lint, queries validate).\n \ +control — manage a cluster via --config: cluster.\n \ +local — no graph; local config & tooling: policy, embed, login, logout, config, \ +version, queries list.\n\ +See the 'Command capabilities' section of the CLI reference for which flags apply where.")] pub(crate) struct Cli { /// Actor id for direct-engine writes; overrides `cli.actor`. No effect on /// remote writes (the server resolves the actor from the bearer token). diff --git a/crates/omnigraph-cli/src/client.rs b/crates/omnigraph-cli/src/client.rs index ca09f88..5d52678 100644 --- a/crates/omnigraph-cli/src/client.rs +++ b/crates/omnigraph-cli/src/client.rs @@ -87,7 +87,7 @@ impl GraphClient { // target straight through, so existing invocations are unchanged. let scope = crate::scope::resolve_scope( &crate::operator::load_operator_config()?, - crate::planes::Plane::Data, + crate::planes::Capability::Any, crate::scope::ScopeFlags { profile, store, server, graph, uri, target }, )?; let (server, graph, uri, target) = ( @@ -134,7 +134,7 @@ impl GraphClient { // through unchanged. let scope = crate::scope::resolve_scope( &crate::operator::load_operator_config()?, - crate::planes::Plane::Data, + crate::planes::Capability::Any, crate::scope::ScopeFlags { profile, store, server, graph, uri, target }, )?; let (server, graph, uri, target) = ( diff --git a/crates/omnigraph-cli/src/helpers.rs b/crates/omnigraph-cli/src/helpers.rs index 6a381b1..5d1cd39 100644 --- a/crates/omnigraph-cli/src/helpers.rs +++ b/crates/omnigraph-cli/src/helpers.rs @@ -490,9 +490,9 @@ pub(crate) fn resolve_local_graph( let graph = resolve_cli_graph(config, cli_uri, cli_target)?; if graph.is_remote { bail!( - "`{}` is a storage-plane command and needs direct storage access; \ - the resolved target is a remote server ({}). Pass the graph's \ - file:// or s3:// URI.", + "`{}` is a direct (storage-native) command and needs direct storage \ + access; the resolved target is a remote server ({}). Pass the \ + graph's file:// or s3:// URI.", operation, graph.uri ); diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index e7cf9bd..988fab9 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -842,7 +842,7 @@ async fn main() -> Result<()> { // (a --profile cluster binding, --store, or operator defaults). let scope = scope::resolve_scope( &operator::load_operator_config()?, - planes::Plane::Storage, + planes::Capability::Direct, scope::ScopeFlags { profile: cli.profile.as_deref(), store: cli.store.as_deref(), @@ -919,7 +919,7 @@ async fn main() -> Result<()> { // RFC-011: no explicit per-command address — consult the scope. let scope = scope::resolve_scope( &operator::load_operator_config()?, - planes::Plane::Storage, + planes::Capability::Direct, scope::ScopeFlags { profile: cli.profile.as_deref(), store: cli.store.as_deref(), @@ -1038,7 +1038,7 @@ async fn main() -> Result<()> { // RFC-011: no explicit per-command address — consult the scope. let scope = scope::resolve_scope( &operator::load_operator_config()?, - planes::Plane::Storage, + planes::Capability::Direct, scope::ScopeFlags { profile: cli.profile.as_deref(), store: cli.store.as_deref(), diff --git a/crates/omnigraph-cli/src/planes.rs b/crates/omnigraph-cli/src/planes.rs index 7c81dfb..dae6440 100644 --- a/crates/omnigraph-cli/src/planes.rs +++ b/crates/omnigraph-cli/src/planes.rs @@ -40,6 +40,63 @@ impl std::fmt::Display for Plane { } } +/// What a command *needs*, in the user-facing vocabulary (RFC-011). This is the +/// language CLI errors and `--help` speak; `Plane` stays the internal classifier +/// (`Capability` is derived from it, so the two cannot drift). +/// +/// - `any` — graph-scoped data; served via a server scope, or direct against a +/// store scope. Accepts `--server`/`--graph`. +/// - `served` — requires a server. Accepts `--server`/`--graph`. +/// - `direct` — storage-native; opens storage directly, never through a server. +/// - `control` — operates on a cluster (control plane). +/// - `local` — addresses no graph at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Capability { + Any, + Served, + Direct, + Control, + Local, +} + +impl Capability { + /// A human phrase for error messages (`` `optimize` is a {…} command ``). + pub(crate) fn describe(self) -> &'static str { + match self { + Capability::Any => "data", + Capability::Served => "served", + Capability::Direct => "direct (storage-native)", + Capability::Control => "cluster control", + Capability::Local => "local", + } + } + + /// `--server`/`--graph` are served-graph addressing: they apply only to the + /// capabilities that reach a graph through a server. + fn accepts_server_addressing(self) -> bool { + matches!(self, Capability::Any | Capability::Served) + } +} + +/// The capability a subcommand needs, derived from its `Plane` (the exhaustive +/// classifier) plus the one Data→Served refinement: `graphs` is remote-only. +/// +/// This reflects *current enforced behavior*, so messages stay truthful: +/// `queries list` is `Local` (reads config today) and `queries validate` is +/// `Direct` (opens a graph directly today). Both converge to the RFC end-state +/// (served / control) only when later slices re-route them. +pub(crate) fn command_capability(cmd: &Command) -> Capability { + if let Command::Graphs { .. } = cmd { + return Capability::Served; + } + match command_plane(cmd) { + Plane::Data => Capability::Any, + Plane::Storage => Capability::Direct, + Plane::Control => Capability::Control, + Plane::Session => Capability::Local, + } +} + /// The plane a subcommand belongs to. Exhaustive — a new `Command` variant /// will not compile until classified. Descends into the nested enums where /// the plane differs per subcommand (`schema plan` is storage while `schema @@ -129,23 +186,75 @@ pub(crate) fn guard_addressing(cli: &Cli) -> Result<()> { if cli.server.is_none() && cli.graph.is_none() { return Ok(()); } - let plane = command_plane(&cli.command); - if plane == Plane::Data { + let capability = command_capability(&cli.command); + if capability.accepts_server_addressing() { return Ok(()); } let label = command_label(&cli.command); - let how = match plane { - // `init` is the one storage verb with no `--target` today (it takes a + let how = match capability { + // `init` is the one direct verb with no `--target` today (it takes a // required positional URI), so its remediation drops the `--target` half. - Plane::Storage => match cli.command { + Capability::Direct => match cli.command { Command::Init { .. } => "Pass a storage URI.", _ => "Use --target , a storage URI, or --cluster --cluster-graph .", }, - Plane::Control => "It operates on a cluster directory (pass --config ).", - Plane::Session => "It does not address a graph.", - Plane::Data => unreachable!("data plane returned early"), + Capability::Control => "It operates on a cluster (pass --config ).", + Capability::Local => "It does not address a graph.", + Capability::Any | Capability::Served => { + unreachable!("served-addressing capabilities returned early") + } }; bail!( - "`{label}` is a {plane}-plane command; --server/--graph address the data plane and do not apply. {how}" + "`{label}` is a {} command; --server/--graph address a served graph and do not apply. {how}", + capability.describe() ); } + +#[cfg(test)] +mod tests { + use clap::Parser; + + use super::*; + + #[test] + fn server_addressing_allowed_exactly_on_any_and_served() { + // The behavior-preservation contract: `--server`/`--graph` apply to the + // served-graph capabilities (`any`, `served`) and nothing else. This is + // the old "Data plane only" allow set, re-expressed — graphs (the one + // Data→Served verb) was already allowed. + assert!(Capability::Any.accepts_server_addressing()); + assert!(Capability::Served.accepts_server_addressing()); + assert!(!Capability::Direct.accepts_server_addressing()); + assert!(!Capability::Control.accepts_server_addressing()); + assert!(!Capability::Local.accepts_server_addressing()); + } + + #[test] + fn command_capability_classifies_representative_verbs() { + let cap = |args: &[&str]| { + command_capability(&Cli::try_parse_from(args).unwrap().command) + }; + assert_eq!(cap(&["omnigraph", "optimize", "graph.omni"]), Capability::Direct); + assert_eq!(cap(&["omnigraph", "schema", "plan", "--schema", "s.pg", "graph.omni"]), Capability::Direct); + assert_eq!(cap(&["omnigraph", "cluster", "status", "--config", "."]), Capability::Control); + assert_eq!(cap(&["omnigraph", "version"]), Capability::Local); + assert_eq!(cap(&["omnigraph", "queries", "list"]), Capability::Local); + } + + #[test] + fn every_capability_describes_distinctly() { + let phrases = [ + Capability::Any.describe(), + Capability::Served.describe(), + Capability::Direct.describe(), + Capability::Control.describe(), + Capability::Local.describe(), + ]; + for (i, a) in phrases.iter().enumerate() { + assert!(!a.is_empty()); + for b in &phrases[i + 1..] { + assert_ne!(a, b); + } + } + } +} diff --git a/crates/omnigraph-cli/src/scope.rs b/crates/omnigraph-cli/src/scope.rs index 19ac48d..692ff0a 100644 --- a/crates/omnigraph-cli/src/scope.rs +++ b/crates/omnigraph-cli/src/scope.rs @@ -10,7 +10,7 @@ //! invocations are unaffected. //! //! The access path (served vs direct) is never chosen here; it falls out of the -//! scope's binding × the verb's plane. The plane→scope capability check rejects +//! scope's binding × the verb's capability. The capability→scope check rejects //! mismatches (e.g. a server scope on a maintenance verb) only on the *new* //! resolution paths. @@ -20,7 +20,7 @@ use color_eyre::Result; use color_eyre::eyre::{bail, eyre}; use crate::operator::{OperatorConfig, ScopeBinding}; -use crate::planes::Plane; +use crate::planes::Capability; pub(crate) const PROFILE_ENV: &str = "OMNIGRAPH_PROFILE"; @@ -48,14 +48,14 @@ pub(crate) struct ScopeFlags<'a> { pub(crate) target: Option<&'a str>, } -/// Resolve the scope for a command on `plane`. Precedence (RFC-011): +/// Resolve the scope for a command with `capability`. Precedence (RFC-011): /// 1. explicit legacy/primitive address (`uri`/`target`/`--server`/`--store`) → passthrough; /// 2. `--profile` / `OMNIGRAPH_PROFILE`; /// 3. flat `defaults.server` + `defaults.default_graph`; /// 4. nothing — downstream behaves as today. pub(crate) fn resolve_scope( op: &OperatorConfig, - plane: Plane, + capability: Capability, flags: ScopeFlags<'_>, ) -> Result { // 1. Any explicit address wins; reproduce today's behavior untouched. @@ -85,7 +85,7 @@ pub(crate) fn resolve_scope( .graph .map(str::to_string) .or_else(|| profile.default_graph.clone()); - return scope_from_binding(op, plane, binding, graph, &format!("profile '{name}'")); + return scope_from_binding(op, capability, binding, graph, &format!("profile '{name}'")); } // 3. Flat default server scope. @@ -96,7 +96,7 @@ pub(crate) fn resolve_scope( .or_else(|| op.default_graph().map(str::to_string)); return scope_from_binding( op, - plane, + capability, ScopeBinding::Server(server.to_string()), graph, "operator defaults", @@ -108,20 +108,20 @@ pub(crate) fn resolve_scope( Ok(ResolvedScope::default()) } -/// Map a resolved binding to the effective tuple, enforcing scope × plane +/// Map a resolved binding to the effective tuple, enforcing scope × capability /// capability (RFC-011): a server scope is served (data only); a cluster scope /// is privileged direct (maintenance/control only); a store scope is direct /// (either). fn scope_from_binding( op: &OperatorConfig, - plane: Plane, + capability: Capability, binding: ScopeBinding, graph: Option, source: &str, ) -> Result { match binding { ScopeBinding::Server(server) => { - if plane == Plane::Storage { + if capability == Capability::Direct { bail!( "this command needs direct storage access, but {source} resolves a \ server scope; name storage explicitly with --store (or a \ @@ -135,7 +135,7 @@ fn scope_from_binding( }) } ScopeBinding::Cluster(cluster) => { - if plane == Plane::Data { + if capability == Capability::Any { bail!( "{source} resolves a cluster scope, which is maintenance-only; run \ data commands through a server, or use --store for ad-hoc \ @@ -200,7 +200,7 @@ mod tests { // A positional URI given → profile/defaults are ignored entirely. let scope = resolve_scope( &op, - Plane::Data, + Capability::Any, ScopeFlags { uri: Some("graph.omni".into()), ..flags() @@ -216,7 +216,7 @@ mod tests { let op = OperatorConfig::default(); let scope = resolve_scope( &op, - Plane::Data, + Capability::Any, ScopeFlags { store: Some("s3://b/g.omni"), ..flags() @@ -229,7 +229,7 @@ mod tests { #[test] fn flat_default_server_drives_data_verbs() { let op = cfg("defaults:\n server: prod\n default_graph: knowledge\nservers:\n prod:\n url: https://x\n"); - let scope = resolve_scope(&op, Plane::Data, flags()).unwrap(); + let scope = resolve_scope(&op, Capability::Any, flags()).unwrap(); assert_eq!(scope.server.as_deref(), Some("prod")); assert_eq!(scope.graph.as_deref(), Some("knowledge")); } @@ -241,7 +241,7 @@ mod tests { ); let scope = resolve_scope( &op, - Plane::Data, + Capability::Any, ScopeFlags { profile: Some("staging"), graph: Some("archive"), @@ -260,7 +260,7 @@ mod tests { ); let scope = resolve_scope( &op, - Plane::Storage, + Capability::Direct, ScopeFlags { profile: Some("admin"), ..flags() @@ -274,7 +274,7 @@ mod tests { #[test] fn server_scope_on_maintenance_verb_errors() { let op = cfg("defaults:\n server: prod\nservers:\n prod:\n url: https://x\n"); - let err = resolve_scope(&op, Plane::Storage, flags()).unwrap_err().to_string(); + let err = resolve_scope(&op, Capability::Direct, flags()).unwrap_err().to_string(); assert!(err.contains("direct storage access"), "{err}"); } @@ -285,7 +285,7 @@ mod tests { ); let err = resolve_scope( &op, - Plane::Data, + Capability::Any, ScopeFlags { profile: Some("admin"), ..flags() @@ -301,7 +301,7 @@ mod tests { let op = OperatorConfig::default(); let err = resolve_scope( &op, - Plane::Data, + Capability::Any, ScopeFlags { profile: Some("nope"), ..flags() @@ -315,7 +315,7 @@ mod tests { #[test] fn no_address_resolves_empty_for_legacy_fallthrough() { let op = OperatorConfig::default(); - let scope = resolve_scope(&op, Plane::Data, flags()).unwrap(); + let scope = resolve_scope(&op, Capability::Any, flags()).unwrap(); assert_eq!(scope, ResolvedScope::default()); } } diff --git a/crates/omnigraph-cli/tests/cli_data.rs b/crates/omnigraph-cli/tests/cli_data.rs index 99a3038..f7fbc7a 100644 --- a/crates/omnigraph-cli/tests/cli_data.rs +++ b/crates/omnigraph-cli/tests/cli_data.rs @@ -164,10 +164,10 @@ fn optimize_with_server_flag_errors_wrong_plane() { let output = output_failure(cli().arg("optimize").arg("--server").arg("prod")); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("`optimize` is a storage-plane command") - && stderr.contains("--server/--graph address the data plane and do not apply") + stderr.contains("`optimize` is a direct (storage-native) command") + && stderr.contains("--server/--graph address a served graph and do not apply") && stderr.contains("Use --target , a storage URI, or --cluster --cluster-graph ."), - "wrong-plane guard message not found; got: {stderr}" + "wrong-capability guard message not found; got: {stderr}" ); } @@ -178,9 +178,9 @@ fn optimize_with_remote_target_errors_storage_plane() { let output = output_failure(cli().arg("optimize").arg("https://graph.example.invalid")); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("`optimize` is a storage-plane command and needs direct storage access") + stderr.contains("`optimize` is a direct (storage-native) command and needs direct storage access") && stderr.contains("remote server"), - "storage-plane remote-target message not found; got: {stderr}" + "direct remote-target message not found; got: {stderr}" ); } @@ -584,12 +584,12 @@ query list_people() { .arg("http://127.0.0.1:8080"), ); let stderr = String::from_utf8_lossy(&output.stderr); - // RFC-010 Slice 1: the storage-plane verbs now share one declared message + // RFC-010/011: the direct (storage-native) verbs share one declared message // (was: "query lint is only supported against local graph URIs …"). assert!( - stderr.contains("`lint` is a storage-plane command and needs direct storage access") + stderr.contains("`lint` is a direct (storage-native) command and needs direct storage access") && stderr.contains("remote server"), - "storage-plane remote-target message not found; got: {stderr}" + "direct remote-target message not found; got: {stderr}" ); } diff --git a/crates/omnigraph-cli/tests/cli_schema_config.rs b/crates/omnigraph-cli/tests/cli_schema_config.rs index f4735c1..9751517 100644 --- a/crates/omnigraph-cli/tests/cli_schema_config.rs +++ b/crates/omnigraph-cli/tests/cli_schema_config.rs @@ -25,22 +25,22 @@ fn version_command_prints_current_cli_version() { } #[test] -fn help_groups_commands_by_plane() { - // RFC-010 Slice 2: `--help` clusters commands by plane (declaration order - // in the Command enum) and explains the planes in an after_help legend. - // Pinned lightly — the legend phrase + the cluster ordering — to avoid - // brittle full-text assertions on clap's help body. +fn help_groups_commands_by_capability() { + // RFC-010 Slice 2 / RFC-011 Slice B: `--help` clusters commands (declaration + // order in the Command enum) and explains the capability each needs in an + // after_help legend. Pinned lightly — the legend phrase + the cluster + // ordering — to avoid brittle full-text assertions on clap's help body. let output = output_success(cli().arg("--help")); let stdout = stdout_string(&output); assert!( - stdout.contains("COMMANDS BY PLANE"), - "plane legend (after_help) missing from --help:\n{stdout}" + stdout.contains("COMMANDS BY CAPABILITY"), + "capability legend (after_help) missing from --help:\n{stdout}" ); // The Commands list precedes the legend, so first occurrences sit in the - // list and must appear in plane order: a data verb, then a storage verb, - // then the control verb. + // list and must appear in order: an `any` data verb, then a `direct` verb, + // then the `control` verb. let pos = |needle: &str| { stdout .find(needle) @@ -48,11 +48,11 @@ fn help_groups_commands_by_plane() { }; assert!( pos("query") < pos("optimize"), - "data commands should be listed before storage commands" + "data (any) commands should be listed before direct commands" ); assert!( pos("optimize") < pos("cluster"), - "storage commands should be listed before the control command" + "direct commands should be listed before the control command" ); } @@ -120,9 +120,9 @@ fn schema_plan_with_server_flag_errors_wrong_plane() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("`schema plan` is a storage-plane command") + stderr.contains("`schema plan` is a direct (storage-native) command") && stderr.contains("Use --target , a storage URI, or --cluster --cluster-graph ."), - "schema plan wrong-plane message not found; got: {stderr}" + "schema plan wrong-capability message not found; got: {stderr}" ); } diff --git a/docs/user/cli/reference.md b/docs/user/cli/reference.md index 44d5ad4..15ce953 100644 --- a/docs/user/cli/reference.md +++ b/docs/user/cli/reference.md @@ -28,23 +28,25 @@ Top-level command families and subcommands. Graph-targeting commands accept a po | `policy validate \| test \| explain` | Cedar tooling. Selects `cli.graph`, else `server.graph`, else top-level `policy.file` | | `version` / `-v` | print `omnigraph 0.3.x` | -## Command planes +## Command capabilities -Every command lives on one **plane**, which determines how it reaches a graph and which addressing flags apply: +Every command declares the **capability** it needs — what it requires to reach a graph — which determines the addressing flags that apply: -- **Data plane** — `query`, `mutate`, `load`, `ingest`, `branch *`, `snapshot`, `export`, `commit *`, `schema show`, `schema apply` (and `graphs list`, remote-only today). Run against a graph **embedded or via a server**: accept a positional `URI` / `--target` / `--server` (+ `--graph` for multi-graph servers). -- **Storage / maintenance plane** — `init`, `optimize`, `repair`, `cleanup`, `schema plan`, `queries validate`, `lint`. Run with **direct storage access** (`file://` / `s3://`), never through a server. They accept a positional `URI` or `--target`, but **not** `--server` / `--graph`, and a `--target` that resolves to a remote (`http(s)://`) server is rejected. (`init` takes only a positional `URI` today — no `--target`.) `optimize` / `repair` / `cleanup` also accept **`--cluster --cluster-graph `**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `/graphs/.omni` layout). -- **Control plane** — `cluster *`. Operates on a cluster directory via `--config `. +- **`any`** — `query`, `mutate`, `load`, `ingest`, `branch *`, `snapshot`, `export`, `commit *`, `schema show`, `schema apply`. Run against a graph **served (via a server) or embedded (direct against a store)**: accept a positional `URI` / `--target` / `--server` (+ `--graph` for multi-graph servers) / `--store` / `--profile`. +- **`served`** — `graphs list`. Requires a server (accepts `--server` / `--profile`). +- **`direct`** — `init`, `optimize`, `repair`, `cleanup`, `schema plan`, `queries validate`, `lint`. Need **direct storage access** (`file://` / `s3://`), never through a server. They accept a positional `URI` or `--target`, but **not** `--server` / `--graph`, and a `--target` that resolves to a remote (`http(s)://`) server is rejected. (`init` takes only a positional `URI` today — no `--target`.) `optimize` / `repair` / `cleanup` also accept **`--cluster --cluster-graph `**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `/graphs/.omni` layout). +- **`control`** — `cluster *`. Operates on a cluster directory via `--config `. +- **`local`** — `policy *`, `embed`, `login`, `logout`, `config`, `version`, `queries list`. Address no graph. These restrictions are enforced and reported, not silent: -- A data-plane addressing flag on a non-data verb fails loudly, e.g.: ``optimize is a storage-plane command; --server/--graph address the data plane and do not apply. Use --target , a storage URI, or --cluster --cluster-graph .`` -- A storage-plane verb pointed at a remote target fails loudly, e.g.: ``optimize is a storage-plane command and needs direct storage access; the resolved target is a remote server (https://…). Pass the graph's file:// or s3:// URI.`` +- A served-graph flag (`--server` / `--graph`) on a verb that doesn't reach a graph through a server fails loudly, e.g.: ``optimize is a direct (storage-native) command; --server/--graph address a served graph and do not apply. Use --target , a storage URI, or --cluster --cluster-graph .`` +- A `direct` verb pointed at a remote target fails loudly, e.g.: ``optimize is a direct (storage-native) command and needs direct storage access; the resolved target is a remote server (https://…). Pass the graph's file:// or s3:// URI.`` - `init` into an **established cluster's** storage layout (`/graphs/.omni` where `` holds `__cluster/state.json`) is refused — graphs in a cluster are created by `cluster apply` (which records ledger / recovery / approvals), not `init`. -To maintain a server-backed graph, run the maintenance verbs from a host with storage access against the graph's storage URI (`--target`, or `--cluster … --cluster-graph …`), out-of-band from the serving process — there are no server routes for `optimize` / `repair` / `cleanup` by design. +To maintain a server-backed graph, run the `direct` verbs from a host with storage access against the graph's storage URI (`--target`, or `--cluster … --cluster-graph …`), out-of-band from the serving process — there are no server routes for `optimize` / `repair` / `cleanup` by design. -`omnigraph --help` lists commands **clustered by plane** (data → storage → control → session) with a plane legend at the bottom. +`omnigraph --help` lists commands with a **capability legend** at the bottom (any / served / direct / control / local). ## Config surfaces diff --git a/docs/user/operations/maintenance.md b/docs/user/operations/maintenance.md index 4f065e5..d9aaa7f 100644 --- a/docs/user/operations/maintenance.md +++ b/docs/user/operations/maintenance.md @@ -1,6 +1,6 @@ # Maintenance: Optimize, Repair & Cleanup -**Addressing.** `optimize`, `repair`, and `cleanup` are **storage-plane** CLI commands: they run with direct storage access against a positional `URI`, `--target`, or **`--cluster --cluster-graph `** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `/graphs/.omni` layout). They never run through a server, and reject `--server` / `--graph` or a `--target` that resolves to a remote (`http(s)://`) URL with a declared error. There are no server routes for them by design — to maintain a server-backed graph, run them out-of-band against the graph's storage URI. See the *Command planes* section of [cli-reference.md](../cli/reference.md). +**Addressing.** `optimize`, `repair`, and `cleanup` are **direct** (storage-native) CLI commands: they run with direct storage access against a positional `URI`, `--target`, or **`--cluster --cluster-graph `** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `/graphs/.omni` layout). They never run through a server, and reject `--server` / `--graph` or a `--target` that resolves to a remote (`http(s)://`) URL with a declared error. There are no server routes for them by design — to maintain a server-backed graph, run them out-of-band against the graph's storage URI. See the *Command capabilities* section of [cli-reference.md](../cli/reference.md). ## `optimize` — non-destructive