mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-18 02:24:27 +02:00
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:
parent
a4d08a4184
commit
7eeced3e88
10 changed files with 191 additions and 79 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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) = (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue