mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-24 02:38:06 +02:00
Resolve graph config by identity, not server mode
Which policy/queries block applies for a graph was decided three different,
mode-dependent ways: single-mode boot used top-level even for a named graph;
multi-mode used per-graph (and silently ignored a top-level queries block); the
CLI used per-graph for a named target. So `queries validate --target prod`
could check a different registry than the single-mode server loaded, and a
named graph's per-graph policy/queries were silently shadowed.
Make config a function of graph IDENTITY: a graph served by NAME
(--target/server.graph, a graphs: entry) uses its own graphs.<name>.{policy,
queries}; a bare URI is anonymous and uses top-level. One rule, applied by
single-mode boot, multi-mode boot, and the CLI — so they can't diverge and the
CLI predicts the server exactly.
No silent ignore: serving a named graph while a top-level policy/queries block
is populated now refuses boot, naming the block (the multi-mode top-level-policy
bail, extended to queries and to single-mode-named). The CLI's `queries
validate` derives the schema URI and the registry from ONE selection, and a
positional URI forces anonymous (ignoring cli.graph) so the two can't come from
different graphs.
BREAKING (released behavior): single mode by name (--target/server.graph) with
top-level policy/queries previously used top-level; it now uses the per-graph
block and refuses boot if top-level is also populated. Bare-URI single mode is
unchanged. Loud, with migration text pointing at graphs.<name>.
- config: resolve_policy_file_for (policy sibling of query_entries_for, no
top-level fallback) + populated_top_level_blocks for the coherence check.
- characterization tests (single-mode named -> per-graph; named + top-level ->
bail; multi-mode top-level queries -> bail; CLI positional-URI -> top-level).
- docs: policy.md, server.md, cli-reference.md.
This commit is contained in:
parent
ad2fc27849
commit
cd570ce59c
8 changed files with 282 additions and 33 deletions
|
|
@ -1679,19 +1679,36 @@ struct QueriesListOutput {
|
|||
queries: Vec<QueriesListItem>,
|
||||
}
|
||||
|
||||
/// Resolve the stored-query registry entries for the selected graph via
|
||||
/// the same `query_entries_for` the server boot uses, so the CLI
|
||||
/// validates exactly the block the server would load. A `--target`
|
||||
/// overrides the configured default graph.
|
||||
fn registry_entries<'a>(
|
||||
config: &'a OmnigraphConfig,
|
||||
target: Option<&str>,
|
||||
) -> &'a std::collections::BTreeMap<String, omnigraph_server::config::QueryEntry> {
|
||||
config.query_entries_for(target.or_else(|| config.cli_graph_name()))
|
||||
/// Resolve the selected graph to `(local URI, registry selection)` from one
|
||||
/// precedence, so a command's schema and its stored-query registry can never
|
||||
/// come from different graphs. A **positional URI is anonymous** (top-level
|
||||
/// registry, ignoring the configured default graph); otherwise `--target`
|
||||
/// or the configured `cli.graph` names the graph (its per-graph block).
|
||||
/// Mirrors the server's single-mode identity rule.
|
||||
fn resolve_selected_graph(
|
||||
config: &OmnigraphConfig,
|
||||
cli_uri: Option<String>,
|
||||
cli_target: Option<&str>,
|
||||
operation: &str,
|
||||
) -> Result<(String, Option<String>)> {
|
||||
let selected = if cli_uri.is_some() {
|
||||
None
|
||||
} else {
|
||||
cli_target
|
||||
.map(str::to_string)
|
||||
.or_else(|| config.cli_graph_name().map(str::to_string))
|
||||
};
|
||||
let uri = resolve_local_uri(config, cli_uri, cli_target, operation)?;
|
||||
Ok((uri, selected))
|
||||
}
|
||||
|
||||
fn load_registry_or_report(config: &OmnigraphConfig, target: Option<&str>) -> Result<QueryRegistry> {
|
||||
QueryRegistry::load(config, registry_entries(config, target)).map_err(|errors| {
|
||||
/// Load the stored-query registry for an already-resolved graph selection
|
||||
/// (`None` = anonymous → top-level; `Some(name)` = that graph's block).
|
||||
fn load_registry_or_report(
|
||||
config: &OmnigraphConfig,
|
||||
selected: Option<&str>,
|
||||
) -> Result<QueryRegistry> {
|
||||
QueryRegistry::load(config, config.query_entries_for(selected)).map_err(|errors| {
|
||||
color_eyre::eyre::eyre!(
|
||||
"stored-query registry failed to load:\n {}",
|
||||
errors
|
||||
|
|
@ -1710,8 +1727,11 @@ async fn execute_queries_validate(
|
|||
json: bool,
|
||||
) -> Result<()> {
|
||||
let config = load_cli_config(config_path)?;
|
||||
let registry = load_registry_or_report(&config, target.as_deref())?;
|
||||
let uri = resolve_local_uri(&config, uri, target.as_deref(), "queries validate")?;
|
||||
// One selection drives both the schema URI and the registry, so a
|
||||
// positional URI and a `--target` can't validate different graphs.
|
||||
let (uri, selected) =
|
||||
resolve_selected_graph(&config, uri, target.as_deref(), "queries validate")?;
|
||||
let registry = load_registry_or_report(&config, selected.as_deref())?;
|
||||
let db = Omnigraph::open(&uri).await?;
|
||||
let report = check(®istry, &db.catalog());
|
||||
|
||||
|
|
@ -1766,7 +1786,10 @@ fn execute_queries_list(
|
|||
json: bool,
|
||||
) -> Result<()> {
|
||||
let config = load_cli_config(config_path)?;
|
||||
let registry = load_registry_or_report(&config, target.as_deref())?;
|
||||
// `list` takes no URI, so the selection is the target or the configured
|
||||
// default graph (named → its per-graph block; else top-level).
|
||||
let selected = target.as_deref().or_else(|| config.cli_graph_name());
|
||||
let registry = load_registry_or_report(&config, selected)?;
|
||||
|
||||
let output = QueriesListOutput {
|
||||
queries: registry
|
||||
|
|
|
|||
|
|
@ -2493,3 +2493,49 @@ fn queries_validate_exits_nonzero_on_duplicate_tool_name() {
|
|||
"duplicate tool name should be reported naming both queries; stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queries_validate_positional_uri_ignores_default_graph() {
|
||||
// A positional URI is anonymous → the schema AND the registry both come
|
||||
// from top-level, even when `cli.graph` names a graph whose per-graph
|
||||
// queries would fail. Pins that the URI and registry can't diverge.
|
||||
let graph = SystemGraph::loaded();
|
||||
graph.write_query(
|
||||
"clean.gq",
|
||||
"query clean($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
||||
);
|
||||
// `Widget` is not in the fixture schema — the default graph's per-graph
|
||||
// query would break validate if it were (wrongly) selected.
|
||||
graph.write_query("broken.gq", "query broken() { match { $w: Widget } return { $w.name } }");
|
||||
let config = graph.write_config(
|
||||
"omnigraph.yaml",
|
||||
concat!(
|
||||
"cli:\n graph: prod\n",
|
||||
"graphs:\n",
|
||||
" prod:\n",
|
||||
" uri: /nonexistent-prod.omni\n",
|
||||
" queries:\n",
|
||||
" broken:\n",
|
||||
" file: ./broken.gq\n",
|
||||
"queries:\n",
|
||||
" clean:\n",
|
||||
" file: ./clean.gq\n",
|
||||
"policy: {}\n",
|
||||
),
|
||||
);
|
||||
// Positional URI = the real loaded graph; selection is anonymous, so the
|
||||
// CLEAN top-level registry validates (not prod's broken one).
|
||||
let output = output_success(
|
||||
cli()
|
||||
.arg("queries")
|
||||
.arg("validate")
|
||||
.arg(graph.path())
|
||||
.arg("--config")
|
||||
.arg(&config),
|
||||
);
|
||||
let stdout = stdout_string(&output);
|
||||
assert!(
|
||||
stdout.contains("OK"),
|
||||
"positional URI must validate the top-level registry, not the cli.graph default; stdout:\n{stdout}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue