feat(cli): plane-grouped --help + clap 4.6.1 (RFC-010 Slice 2) (#220)

* chore(deps): bump clap to 4.6.1

Workspace constraint "4" → "4.6" so the resolver picks up the 4.6 line
(a plain `cargo update` stayed on 4.5.x). clap 4.5.58 → 4.6.1
(clap_builder 4.6.0, clap_derive 4.6.1). Minor bump, no API breakage; the
workspace builds and all CLI suites pass unchanged.

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

* feat(cli): group --help by plane (RFC-010 Slice 2)

Slice 1 declared the planes (the command_plane table + the wrong-plane
guard); this makes them visible in `--help`. clap can't print labeled
heading rows between subcommand groups (verified against the source —
help_heading is args-only, {subcommands} is one flat block), so per the
chosen approach: cluster + legend.

- Reorder the `Command` enum into plane bands (clap lists subcommands in
  declaration order): data (query, mutate, load, branch, snapshot, export,
  commit, schema, graphs) → storage/local-graph ops (init, optimize,
  repair, cleanup, lint, queries) → control (cluster) → session (policy,
  embed, login, logout, config, version). No magic display_order numbers —
  the source order IS the help order, with band comments for readers. The
  band placement matches `command_plane` (lint/queries are storage-plane:
  they reject --server), so the help grouping and the guard agree.
- Add an `after_help` legend on `Cli` naming the planes. Written to
  describe the planes (not enumerate every command) so it doesn't drift.

Help-polish (post-review): hide the deprecated `ingest` from the list
(still a valid command); trim the long `login` and `--as` descriptions to
one line each so the columns don't blow up.

The behavioral source of truth for planes stays `planes::command_plane`;
this ordering is its cosmetic counterpart.

Test: `help_groups_commands_by_plane` pins the legend phrase + the cluster
ordering (query < optimize < cluster). Doc: a line under cli-reference's
*Command planes* section.

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

* feat(cli): qualify mixed-plane commands in the --help legend

Addresses the Greptile P2 on #220: the legend placed `schema` entirely in
Data and `queries` entirely in Storage, but per `command_plane` the
subcommands differ — `schema plan` is storage-plane (rejects --server) and
`queries list` is session (no graph). A user reading the legend then running
`schema plan --server` would hit a rejection contradicting it. The Commands
list is one entry per top-level command (necessarily coarse), so the legend
carries the nuance: `schema [plan: storage]` and `queries [list: session]`.

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-14 01:49:40 +03:00 committed by GitHub
parent 4187d56f8a
commit d6cf5b298c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 239 additions and 191 deletions

24
Cargo.lock generated
View file

@ -83,9 +83,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@ -104,9 +104,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@ -1314,9 +1314,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.58"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
@ -1324,9 +1324,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.58"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@ -1336,9 +1336,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
@ -5323,9 +5323,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]

View file

@ -48,7 +48,7 @@ pest = "2"
pest_derive = "2"
thiserror = "2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net", "signal", "sync"] }
clap = { version = "4", features = ["derive"] }
clap = { version = "4.6", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"

View file

@ -9,15 +9,24 @@ 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`.
#[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.")]
pub(crate) struct Cli {
/// Actor identity for direct-engine writes (MR-722). Overrides
/// `cli.actor` from `omnigraph.yaml`. When the configured policy
/// is in effect, Cedar evaluates this actor against the requested
/// action and scope; with policy configured but neither this flag
/// nor `cli.actor` set, the engine-layer footgun guard fires and
/// the write is denied (no silent bypass). Has no effect on remote
/// HTTP writes — those resolve their actor server-side from the
/// bearer token.
/// Actor id for direct-engine writes; overrides `cli.actor`. No effect on
/// remote writes (the server resolves the actor from the bearer token).
/// With a policy configured but no actor set, the write is denied — see
/// docs/user/policy.md.
#[arg(long = "as", global = true, value_name = "ACTOR")]
pub(crate) as_actor: Option<String>,
@ -38,170 +47,7 @@ pub(crate) struct Cli {
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
/// Print the CLI version
Version,
/// Store a bearer token for a named server in ~/.omnigraph/credentials
/// (0600). Token from --token or one line on stdin:
/// `echo $TOKEN | omnigraph login prod`. The keyed token applies to
/// requests whose URL matches the server's `url` in the operator
/// config's `servers:` map.
Login {
/// Server name (keys the credential; declare its url under
/// `servers:` in ~/.omnigraph/config.yaml)
name: String,
/// The token. Prefer piping via stdin over this flag (shell
/// history).
#[arg(long)]
token: Option<String>,
#[arg(long)]
json: bool,
},
/// Legacy-config tooling (RFC-008): split omnigraph.yaml into its
/// two destinations.
Config {
#[command(subcommand)]
command: ConfigCommand,
},
/// Remove a named server's stored credential. Idempotent.
Logout {
name: String,
#[arg(long)]
json: bool,
},
/// Generate, clean, or refresh explicit seed embeddings
Embed(EmbedArgs),
/// Initialize a new graph from a schema
Init {
#[arg(long)]
schema: PathBuf,
/// Graph URI (local path or s3://)
uri: String,
/// Overwrite existing schema artifacts at the URI. Without
/// this flag, init refuses to touch a URI that already holds
/// `_schema.pg`, `_schema.ir.json`, or `__schema_state.json`
/// — closes the re-init footgun (MR-668 follow-up). With the
/// flag, the operator opts in to destructive semantics.
#[arg(long)]
force: bool,
},
/// Load data into a graph (local or remote)
Load {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
/// Target branch (defaults to main). Without --from it must exist.
#[arg(long)]
branch: Option<String>,
/// Base branch to fork --branch from when it doesn't exist yet.
/// Without this flag a missing branch is an error, never a fork.
#[arg(long)]
from: Option<String>,
/// How existing rows are handled: overwrite | append | merge.
/// Required — overwrite is destructive, so there is no default.
#[arg(long)]
mode: CliLoadMode,
#[arg(long)]
json: bool,
},
/// Deprecated alias of `load --from <base>` (defaults: --mode merge, --from main)
Ingest {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
#[arg(long)]
branch: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long, default_value = "merge")]
mode: CliLoadMode,
#[arg(long)]
json: bool,
},
/// Branch operations
Branch {
#[command(subcommand)]
command: BranchCommand,
},
/// Schema planning operations
Schema {
#[command(subcommand)]
command: SchemaCommand,
},
/// Validate queries against a schema (offline) or repo (repo-backed).
///
/// Canonical name is `lint` (matches the `omnigraph_compiler::lint`
/// module and the `OG-XXX-NNN` lint-code vocabulary). Replaces the
/// deprecated `omnigraph query lint` / `omnigraph query check` /
/// `omnigraph check` invocations — each is kept as an argv-level
/// shim that prints a one-line stderr warning and rewrites to
/// `omnigraph lint`. Aliases are deliberately *not* exposed via
/// clap's `visible_alias` because that would advertise two
/// equivalent canonical names, which agents emit interchangeably
/// (see MR-981).
Lint {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
query: PathBuf,
#[arg(long)]
schema: Option<PathBuf>,
#[arg(long)]
json: bool,
},
/// Operate on the server-side stored-query registry (`queries:`).
Queries {
#[command(subcommand)]
command: QueriesCommand,
},
/// Show graph snapshot
Snapshot {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
#[arg(long)]
json: bool,
},
/// Export a full graph snapshot as JSONL
Export {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
#[arg(long, hide = true)]
jsonl: bool,
#[arg(long = "type")]
type_names: Vec<String>,
#[arg(long = "table")]
table_keys: Vec<String>,
},
/// Commit history operations
Commit {
#[command(subcommand)]
command: CommitCommand,
},
// ── Data plane ── run against a graph (embedded or via --server).
/// Execute a read query against a branch or snapshot.
///
/// Canonical read endpoint. The previous name `omnigraph read` is
@ -274,10 +120,115 @@ pub(crate) enum Command {
#[arg()]
alias_args: Vec<String>,
},
/// Policy administration and diagnostics
Policy {
/// Load data into a graph (local or remote)
Load {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
/// Target branch (defaults to main). Without --from it must exist.
#[arg(long)]
branch: Option<String>,
/// Base branch to fork --branch from when it doesn't exist yet.
/// Without this flag a missing branch is an error, never a fork.
#[arg(long)]
from: Option<String>,
/// How existing rows are handled: overwrite | append | merge.
/// Required — overwrite is destructive, so there is no default.
#[arg(long)]
mode: CliLoadMode,
#[arg(long)]
json: bool,
},
/// Deprecated alias of `load --from <base>` (defaults: --mode merge, --from main)
#[command(hide = true)]
Ingest {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
#[arg(long)]
branch: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long, default_value = "merge")]
mode: CliLoadMode,
#[arg(long)]
json: bool,
},
/// Branch operations
Branch {
#[command(subcommand)]
command: PolicyCommand,
command: BranchCommand,
},
/// Show graph snapshot
Snapshot {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
#[arg(long)]
json: bool,
},
/// Export a full graph snapshot as JSONL
Export {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
#[arg(long, hide = true)]
jsonl: bool,
#[arg(long = "type")]
type_names: Vec<String>,
#[arg(long = "table")]
table_keys: Vec<String>,
},
/// Commit history operations
Commit {
#[command(subcommand)]
command: CommitCommand,
},
/// Schema planning operations
Schema {
#[command(subcommand)]
command: SchemaCommand,
},
/// Manage graphs on a multi-graph server (MR-668)
Graphs {
#[command(subcommand)]
command: GraphsCommand,
},
// ── Storage / local graph ops ── direct storage or local files; reject --server.
/// Initialize a new graph from a schema
Init {
#[arg(long)]
schema: PathBuf,
/// Graph URI (local path or s3://)
uri: String,
/// Overwrite existing schema artifacts at the URI. Without
/// this flag, init refuses to touch a URI that already holds
/// `_schema.pg`, `_schema.ir.json`, or `__schema_state.json`
/// — closes the re-init footgun (MR-668 follow-up). With the
/// flag, the operator opts in to destructive semantics.
#[arg(long)]
force: bool,
},
/// Compact small Lance fragments in every table of the graph
Optimize {
@ -331,16 +282,79 @@ pub(crate) enum Command {
#[arg(long)]
json: bool,
},
/// Validate queries against a schema (offline) or repo (repo-backed).
///
/// Canonical name is `lint` (matches the `omnigraph_compiler::lint`
/// module and the `OG-XXX-NNN` lint-code vocabulary). Replaces the
/// deprecated `omnigraph query lint` / `omnigraph query check` /
/// `omnigraph check` invocations — each is kept as an argv-level
/// shim that prints a one-line stderr warning and rewrites to
/// `omnigraph lint`. Aliases are deliberately *not* exposed via
/// clap's `visible_alias` because that would advertise two
/// equivalent canonical names, which agents emit interchangeably
/// (see MR-981).
Lint {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
query: PathBuf,
#[arg(long)]
schema: Option<PathBuf>,
#[arg(long)]
json: bool,
},
/// Operate on the server-side stored-query registry (`queries:`).
Queries {
#[command(subcommand)]
command: QueriesCommand,
},
// ── Control plane ── manage a cluster directory (--config <dir>).
/// Validate and plan read-only cluster configuration.
Cluster {
#[command(subcommand)]
command: ClusterCommand,
},
/// Manage graphs on a multi-graph server (MR-668)
Graphs {
// ── Session / config ── no graph addressing; local tooling.
/// Policy administration and diagnostics
Policy {
#[command(subcommand)]
command: GraphsCommand,
command: PolicyCommand,
},
/// Generate, clean, or refresh explicit seed embeddings
Embed(EmbedArgs),
/// Store a bearer token for a named server (0600 credentials file). Token
/// via --token or piped on stdin; see the CLI reference for token resolution.
Login {
/// Server name (keys the credential; declare its url under
/// `servers:` in ~/.omnigraph/config.yaml)
name: String,
/// The token. Prefer piping via stdin over this flag (shell
/// history).
#[arg(long)]
token: Option<String>,
#[arg(long)]
json: bool,
},
/// Remove a named server's stored credential. Idempotent.
Logout {
name: String,
#[arg(long)]
json: bool,
},
/// Legacy-config tooling (RFC-008): split omnigraph.yaml into its
/// two destinations.
Config {
#[command(subcommand)]
command: ConfigCommand,
},
/// Print the CLI version
Version,
}
#[derive(Debug, Subcommand)]

View file

@ -24,6 +24,38 @@ 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.
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}"
);
// 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.
let pos = |needle: &str| {
stdout
.find(needle)
.unwrap_or_else(|| panic!("'{needle}' not found in --help:\n{stdout}"))
};
assert!(
pos("query") < pos("optimize"),
"data commands should be listed before storage commands"
);
assert!(
pos("optimize") < pos("cluster"),
"storage commands should be listed before the control command"
);
}
#[test]
fn init_creates_graph_successfully_on_missing_local_directory() {
let temp = tempdir().unwrap();

View file

@ -43,6 +43,8 @@ These restrictions are enforced and reported, not silent:
To maintain a server-backed graph, run the maintenance verbs from a host with storage access against the graph's storage URI (or `--target`), 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.
## Config surfaces
Two config surfaces with single owners (RFC-007/RFC-008), plus a zero-config