feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)

* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)

User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.

- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
  the existing exhaustive `command_plane` classifier (which stays as the drift
  guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
  allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
  capability-worded message. The mapping reflects *current* behavior (`queries
  list` → Local, `queries validate` → Direct); it converges to the RFC end-state
  table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
  addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
  maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
  (storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
  tests proving the allow set is exactly {Any, Served} (behavior-preservation),
  the per-verb mapping, and distinct capability phrases.

Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(cli): capability vocabulary in the CLI reference + maintenance addressing

Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Andrew Altshuler 2026-06-15 03:02:07 +03:00 committed by GitHub
parent a4d08a4184
commit 7eeced3e88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 191 additions and 79 deletions

View file

@ -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).

View file

@ -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) = (

View file

@ -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
);

View file

@ -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(),

View file

@ -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 <name>, a storage URI, or --cluster <dir> --cluster-graph <id>.",
},
Plane::Control => "It operates on a cluster directory (pass --config <dir>).",
Plane::Session => "It does not address a graph.",
Plane::Data => unreachable!("data plane returned early"),
Capability::Control => "It operates on a cluster (pass --config <dir>).",
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);
}
}
}
}

View file

@ -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<ResolvedScope> {
// 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<String>,
source: &str,
) -> Result<ResolvedScope> {
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 <uri> (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 <uri> 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());
}
}

View file

@ -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 <name>, a storage URI, or --cluster <dir> --cluster-graph <id>."),
"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}"
);
}

View file

@ -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 <name>, a storage URI, or --cluster <dir> --cluster-graph <id>."),
"schema plan wrong-plane message not found; got: {stderr}"
"schema plan wrong-capability message not found; got: {stderr}"
);
}

View file

@ -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 <dir|s3://…> --cluster-graph <id>`**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `<storage>/graphs/<id>.omni` layout).
- **Control plane**`cluster *`. Operates on a cluster directory via `--config <dir>`.
- **`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 <dir|s3://…> --cluster-graph <id>`**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `<storage>/graphs/<id>.omni` layout).
- **`control`** — `cluster *`. Operates on a cluster directory via `--config <dir>`.
- **`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 <name>, a storage URI, or --cluster <dir> --cluster-graph <id>.``
- 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 <name>, a storage URI, or --cluster <dir> --cluster-graph <id>.``
- 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 (`<root>/graphs/<id>.omni` where `<root>` 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

View file

@ -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 <dir|s3://…> --cluster-graph <id>`** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `<storage>/graphs/<id>.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 <dir|s3://…> --cluster-graph <id>`** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `<storage>/graphs/<id>.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