mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-30 02:49:39 +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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue