feat(cli): RFC-011 Slice A — additive scope/profile addressing (#235)
Some checks failed
CI / Classify Changes (push) Has been cancelled
CI / Check AGENTS.md Links (push) Has been cancelled
CI / Container Entrypoint (push) Has been cancelled
Release Edge / Prepare edge release (push) Has been cancelled
CI / Test Workspace (push) Has been cancelled
CI / Test omnigraph-server --features aws (push) Has been cancelled
CI / RustFS S3 Integration (push) Has been cancelled
Release Edge / Build edge omnigraph-linux-x86_64 (push) Has been cancelled
Release Edge / Build edge omnigraph-macos-arm64 (push) Has been cancelled
Release Edge / Build edge omnigraph-windows-x86_64 (push) Has been cancelled
Release Edge / Smoke Windows installer (push) Has been cancelled

* feat(cli): RFC-011 Slice A — operator-config scope structs (profiles/clusters/defaults)

Additive operator-config surface for the RFC-011 scope model. No behavior
change yet — these structs are parsed but not consumed until the scope
resolver lands.

- OperatorConfig gains `profiles:` (name → OperatorProfile) and `clusters:`
  (name → OperatorCluster { root }) — the latter the only place a storage
  root appears in operator config (RFC-011 storage-root rule).
- OperatorDefaults gains `server` and `default_graph` (the flat-default scope).
- OperatorProfile binds one of {server, cluster, store} + default_graph;
  `binding()` validates exactly-one on use and returns a ScopeBinding.
- Accessors profile()/cluster_root()/default_server()/default_graph();
  unknown-key warnings extended to the new blocks (forward-compat preserved —
  old configs still load, new keys are no longer "unknown").

Tests: parse profiles/clusters/scope-defaults, binding rejects zero/multiple
entities, unknown keys in a profile warn.

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

* feat(cli): RFC-011 Slice A — scope resolver + --profile/--store, wired (additive)

Translate the new scope inputs into the existing addressing tuple, in front of
the unchanged resolvers. Purely additive: an explicit address
(--uri/--target/--server/--store) passes straight through, so every existing
invocation is byte-for-byte unchanged.

- scope.rs: resolve_scope() with the RFC-011 precedence (explicit > --profile /
  OMNIGRAPH_PROFILE > flat defaults.server), producing the effective
  (server, graph, uri, target) for data verbs and (cluster, cluster_graph) for
  maintenance. Plane×scope capability check (server scope rejected on a
  maintenance verb; cluster scope rejected on a data verb; store rejects --graph)
  fires only on the new paths. 9 unit tests.
- cli.rs: global --profile <NAME> and --store <URI>. (--graph keeps
  requires=server for now; profile/default graph comes from default_graph —
  profile+--graph override is deferred to the --cluster-graph rework.)
- client.rs: the two GraphClient factories call resolve_scope (Plane::Data) up
  front; the explicit branch reproduces today's behavior exactly.
- main.rs: the 15 data call sites forward --profile/--store; the 3 maintenance
  verbs consult the scope (Plane::Storage) only when no explicit per-command
  address is given, so cluster-binding profiles and --store reach
  optimize/repair/cleanup.

Verified: the full omnigraph-cli suite (221 tests) stays green untouched.

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

* test+docs(cli): RFC-011 Slice A — end-to-end scope test + reference docs

- cli_data.rs: prove --store and a --profile store binding drive a read
  identically to the legacy positional URI (the additive-coexistence contract),
  end to end against a local graph (no server needed).
- cli/reference.md: document profiles/clusters/defaults.server/default_graph,
  the --profile/--store flags, and a "Scopes & profiles" section; note the model
  coexists with legacy addressing (nothing removed yet).

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 02:37:55 +03:00 committed by GitHub
parent ceb37dd4cb
commit a4d08a4184
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 777 additions and 28 deletions

View file

@ -49,6 +49,7 @@ mod cli;
mod client;
mod helpers;
mod output;
mod scope;
mod planes;
use cli::*;
use helpers::*;
@ -185,6 +186,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let branch = resolve_branch(&config, branch, None, "main");
let payload = client
@ -219,6 +222,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let branch = resolve_branch(&config, branch, None, "main");
let from = resolve_branch(&config, from, None, "main");
@ -248,6 +253,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let from = resolve_branch(&config, from, None, "main");
let payload = client.branch_create_from(&from, &name).await?;
@ -270,6 +277,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let payload = client.branch_list().await?;
if json {
@ -295,6 +304,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let payload = client.branch_delete(&name).await?;
if json {
@ -319,6 +330,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let into = resolve_branch(&config, into, None, "main");
let payload = client.branch_merge(&source, &into).await?;
@ -349,6 +362,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let payload = client.list_commits(branch.as_deref()).await?;
if json {
@ -371,6 +386,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let commit = client.get_commit(&commit_id).await?;
if json {
@ -427,6 +444,8 @@ async fn main() -> Result<()> {
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let schema_source = fs::read_to_string(&schema)?;
// The stored-query registry check is an embedded-only concern
@ -467,6 +486,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let output = client.schema_source().await?;
if json {
@ -521,6 +542,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let branch = resolve_branch(&config, branch, None, "main");
let payload = client.snapshot(&branch).await?;
@ -546,6 +569,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let branch = resolve_branch(&config, branch, None, "main");
if jsonl {
@ -636,6 +661,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target_name,
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let query_source = resolve_query_source(
&config,
@ -714,6 +741,8 @@ async fn main() -> Result<()> {
uri,
target_name,
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let query_source = resolve_query_source(
&config,
@ -798,15 +827,41 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"optimize",
)
.await?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"optimize",
)
.await?
} else {
// RFC-011: no explicit per-command address — consult the scope
// (a --profile cluster binding, --store, or operator defaults).
let scope = scope::resolve_scope(
&operator::load_operator_config()?,
planes::Plane::Storage,
scope::ScopeFlags {
profile: cli.profile.as_deref(),
store: cli.store.as_deref(),
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"optimize",
)
.await?
};
let db = Omnigraph::open(&uri).await?;
let stats = db.optimize().await?;
if json {
@ -850,15 +905,40 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"repair",
)
.await?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"repair",
)
.await?
} else {
// RFC-011: no explicit per-command address — consult the scope.
let scope = scope::resolve_scope(
&operator::load_operator_config()?,
planes::Plane::Storage,
scope::ScopeFlags {
profile: cli.profile.as_deref(),
store: cli.store.as_deref(),
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"repair",
)
.await?
};
let db = Omnigraph::open(&uri).await?;
let stats = db
.repair(omnigraph::db::RepairOptions { confirm, force })
@ -944,15 +1024,40 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"cleanup",
)
.await?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"cleanup",
)
.await?
} else {
// RFC-011: no explicit per-command address — consult the scope.
let scope = scope::resolve_scope(
&operator::load_operator_config()?,
planes::Plane::Storage,
scope::ScopeFlags {
profile: cli.profile.as_deref(),
store: cli.store.as_deref(),
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"cleanup",
)
.await?
};
let older_than_dur = older_than.as_deref().map(parse_duration_arg).transpose()?;
@ -1088,6 +1193,8 @@ async fn main() -> Result<()> {
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
let payload = client.list_graphs().await?;
if json {