mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-12 01:45:14 +02:00
Merge pull request #183 from ModernRelay/feat/cluster-query-discovery
feat(cluster): Terraform-shaped query declaration; drop ./ path noise
This commit is contained in:
commit
4bd763f4b8
8 changed files with 370 additions and 104 deletions
|
|
@ -415,7 +415,141 @@ struct StateConfig {
|
|||
struct GraphConfig {
|
||||
schema: PathBuf,
|
||||
#[serde(default)]
|
||||
queries: BTreeMap<String, QueryConfig>,
|
||||
queries: QueriesDecl,
|
||||
}
|
||||
|
||||
/// How a graph declares its stored queries. Terraform-style: the `.gq`
|
||||
/// files ARE the declaration — point at them (or a directory) and every
|
||||
/// `query <name>` they contain is discovered. The explicit name->file map
|
||||
/// remains for fine-grained control.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum QueriesDecl {
|
||||
/// `queries: ./queries/` — a directory (top-level `*.gq`, sorted) or a
|
||||
/// single `.gq` file; every declaration inside is registered.
|
||||
Discover(PathBuf),
|
||||
/// `queries: [./queries/, ./extra.gq]` — several directories/files.
|
||||
DiscoverMany(Vec<PathBuf>),
|
||||
/// `queries: { name: { file: ... } }` — explicit registry.
|
||||
Explicit(BTreeMap<String, QueryConfig>),
|
||||
}
|
||||
|
||||
impl Default for QueriesDecl {
|
||||
fn default() -> Self {
|
||||
QueriesDecl::Explicit(BTreeMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand a graph's query declaration into the canonical name->file map.
|
||||
/// Discovery reads and parses each `.gq`; unreadable or unparseable files
|
||||
/// and duplicate query names are loud validation errors — a declaration the
|
||||
/// tool cannot enumerate is broken, not partially usable.
|
||||
fn resolve_query_decls(
|
||||
config_dir: &Path,
|
||||
graph_id: &str,
|
||||
decl: &QueriesDecl,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) -> (BTreeMap<String, QueryConfig>, BTreeMap<PathBuf, String>) {
|
||||
let paths: Vec<PathBuf> = match decl {
|
||||
QueriesDecl::Explicit(map) => {
|
||||
return (
|
||||
map.iter()
|
||||
.map(|(name, config)| {
|
||||
(name.clone(), QueryConfig { file: config.file.clone() })
|
||||
})
|
||||
.collect(),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
}
|
||||
QueriesDecl::Discover(path) => vec![path.clone()],
|
||||
QueriesDecl::DiscoverMany(paths) => paths.clone(),
|
||||
};
|
||||
|
||||
let mut files: Vec<(PathBuf, PathBuf)> = Vec::new(); // (declared-relative, resolved)
|
||||
for declared in &paths {
|
||||
let resolved = resolve_config_path(config_dir, declared);
|
||||
if resolved.is_dir() {
|
||||
let mut entries: Vec<PathBuf> = match fs::read_dir(&resolved) {
|
||||
Ok(read) => read
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "gq"))
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
diagnostics.push(Diagnostic::error(
|
||||
"query_dir_unreadable",
|
||||
format!("graphs.{graph_id}.queries"),
|
||||
format!("could not list query directory '{}': {err}", resolved.display()),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
entries.sort();
|
||||
if entries.is_empty() {
|
||||
diagnostics.push(Diagnostic::warning(
|
||||
"query_dir_empty",
|
||||
format!("graphs.{graph_id}.queries"),
|
||||
format!("query directory '{}' contains no .gq files", resolved.display()),
|
||||
));
|
||||
}
|
||||
for path in entries {
|
||||
let relative = declared.join(path.file_name().expect("dir entries have names"));
|
||||
files.push((relative, path));
|
||||
}
|
||||
} else {
|
||||
files.push((declared.clone(), resolved));
|
||||
}
|
||||
}
|
||||
|
||||
let mut registry: BTreeMap<String, QueryConfig> = BTreeMap::new();
|
||||
let mut origin: BTreeMap<String, PathBuf> = BTreeMap::new();
|
||||
// Content read once at discovery and handed to the caller — the per-query
|
||||
// digest/typecheck pass reuses it instead of re-reading (no N+1 reads, no
|
||||
// window for the file to change between enumeration and validation).
|
||||
let mut contents: BTreeMap<PathBuf, String> = BTreeMap::new();
|
||||
for (declared, resolved) in files {
|
||||
let source = match fs::read_to_string(&resolved) {
|
||||
Ok(source) => source,
|
||||
Err(err) => {
|
||||
diagnostics.push(Diagnostic::error(
|
||||
"query_file_missing",
|
||||
format!("graphs.{graph_id}.queries"),
|
||||
format!("could not read query file '{}': {err}", resolved.display()),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let parsed = match parse_query(&source) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
diagnostics.push(Diagnostic::error(
|
||||
"query_parse_error",
|
||||
format!("graphs.{graph_id}.queries"),
|
||||
format!("'{}' does not parse: {err}", resolved.display()),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for query_decl in &parsed.queries {
|
||||
let name = query_decl.name.clone();
|
||||
if let Some(previous) = origin.get(&name) {
|
||||
diagnostics.push(Diagnostic::error(
|
||||
"duplicate_query_name",
|
||||
format!("graphs.{graph_id}.queries.{name}"),
|
||||
format!(
|
||||
"query '{name}' is declared in both '{}' and '{}'",
|
||||
previous.display(),
|
||||
declared.display()
|
||||
),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
origin.insert(name.clone(), declared.clone());
|
||||
registry.insert(name, QueryConfig { file: declared.clone() });
|
||||
}
|
||||
contents.insert(declared, source);
|
||||
}
|
||||
(registry, contents)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
@ -3600,7 +3734,9 @@ fn load_desired(config_dir: &Path) -> LoadOutcome {
|
|||
}
|
||||
});
|
||||
|
||||
for (query_name, query) in &graph.queries {
|
||||
let (graph_queries, query_contents) =
|
||||
resolve_query_decls(&config_dir, graph_id, &graph.queries, &mut diagnostics);
|
||||
for (query_name, query) in &graph_queries {
|
||||
validate_id(
|
||||
"query name",
|
||||
&format!("graphs.{graph_id}.queries.{query_name}"),
|
||||
|
|
@ -3618,7 +3754,11 @@ fn load_desired(config_dir: &Path) -> LoadOutcome {
|
|||
});
|
||||
|
||||
let query_path = resolve_config_path(&config_dir, &query.file);
|
||||
match fs::read_to_string(&query_path) {
|
||||
let source = match query_contents.get(&query.file) {
|
||||
Some(cached) => Ok(cached.clone()),
|
||||
None => fs::read_to_string(&query_path),
|
||||
};
|
||||
match source {
|
||||
Ok(source) => {
|
||||
let digest = sha256_hex(source.as_bytes());
|
||||
graph_query_digests
|
||||
|
|
@ -7560,6 +7700,115 @@ policies:
|
|||
);
|
||||
}
|
||||
|
||||
// ---- query discovery (Terraform-style declaration) ----
|
||||
|
||||
#[test]
|
||||
fn queries_directory_discovers_every_declaration() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("people.pg"), "\nnode Person {\n name: String @key\n}\n").unwrap();
|
||||
fs::create_dir(dir.path().join("queries")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join("queries/people.gq"),
|
||||
"\nquery find_person($name: String) {\n match { $p: Person { name: $name } }\n return { $p.name }\n}\n\nquery all_people() {\n match { $p: Person }\n return { $p.name }\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join("queries/extra.gq"),
|
||||
"\nquery count_people() {\n match { $p: Person }\n return { count($p) }\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(dir.path().join("queries/notes.txt"), "ignored").unwrap();
|
||||
fs::write(
|
||||
dir.path().join("cluster.yaml"),
|
||||
"version: 1\ngraphs:\n knowledge:\n schema: ./people.pg\n queries: ./queries/\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let out = validate_config_dir(dir.path());
|
||||
assert!(out.ok, "{:?}", out.diagnostics);
|
||||
let names: Vec<&str> = out
|
||||
.resource_digests
|
||||
.keys()
|
||||
.filter_map(|address| address.strip_prefix("query.knowledge."))
|
||||
.collect();
|
||||
assert_eq!(names, vec!["all_people", "count_people", "find_person"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queries_list_and_single_file_forms_discover() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("people.pg"), "\nnode Person {\n name: String @key\n}\n").unwrap();
|
||||
fs::write(
|
||||
dir.path().join("a.gq"),
|
||||
"\nquery find_person($name: String) {\n match { $p: Person { name: $name } }\n return { $p.name }\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join("b.gq"),
|
||||
"\nquery all_people() {\n match { $p: Person }\n return { $p.name }\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join("cluster.yaml"),
|
||||
"version: 1\ngraphs:\n knowledge:\n schema: ./people.pg\n queries: [./a.gq, ./b.gq]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let out = validate_config_dir(dir.path());
|
||||
assert!(out.ok, "{:?}", out.diagnostics);
|
||||
assert!(out.resource_digests.contains_key("query.knowledge.find_person"));
|
||||
assert!(out.resource_digests.contains_key("query.knowledge.all_people"));
|
||||
|
||||
// Single-file string form
|
||||
fs::write(
|
||||
dir.path().join("cluster.yaml"),
|
||||
"version: 1\ngraphs:\n knowledge:\n schema: ./people.pg\n queries: ./a.gq\n",
|
||||
)
|
||||
.unwrap();
|
||||
let out = validate_config_dir(dir.path());
|
||||
assert!(out.ok, "{:?}", out.diagnostics);
|
||||
assert!(out.resource_digests.contains_key("query.knowledge.find_person"));
|
||||
assert!(!out.resource_digests.contains_key("query.knowledge.all_people"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_discovery_rejects_duplicates_and_parse_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("people.pg"), "\nnode Person {\n name: String @key\n}\n").unwrap();
|
||||
let decl = "\nquery find_person($name: String) {\n match { $p: Person { name: $name } }\n return { $p.name }\n}\n";
|
||||
fs::write(dir.path().join("a.gq"), decl).unwrap();
|
||||
fs::write(dir.path().join("b.gq"), decl).unwrap();
|
||||
fs::write(
|
||||
dir.path().join("cluster.yaml"),
|
||||
"version: 1\ngraphs:\n knowledge:\n schema: ./people.pg\n queries: [./a.gq, ./b.gq]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let out = validate_config_dir(dir.path());
|
||||
assert!(!out.ok);
|
||||
assert!(
|
||||
out.diagnostics
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.code == "duplicate_query_name"),
|
||||
"{:?}",
|
||||
out.diagnostics
|
||||
);
|
||||
|
||||
fs::write(dir.path().join("broken.gq"), "query {{{ nope").unwrap();
|
||||
fs::write(
|
||||
dir.path().join("cluster.yaml"),
|
||||
"version: 1\ngraphs:\n knowledge:\n schema: ./people.pg\n queries: ./broken.gq\n",
|
||||
)
|
||||
.unwrap();
|
||||
let out = validate_config_dir(dir.path());
|
||||
assert!(!out.ok);
|
||||
assert!(
|
||||
out.diagnostics
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.code == "query_parse_error"),
|
||||
"{:?}",
|
||||
out.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_warns_on_pending_recovery_sidecar() {
|
||||
let dir = fixture();
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ cli:
|
|||
query:
|
||||
roots: [<dir>, …] # search path for .gq files
|
||||
auth:
|
||||
env_file: ./.env.omni
|
||||
env_file: .env.omni
|
||||
aliases:
|
||||
<alias>:
|
||||
# accepted values: `read` / `query` (read alias), `change` / `mutate`
|
||||
|
|
@ -70,20 +70,20 @@ aliases:
|
|||
queries: # top-level registry — applies only to a bare-URI (anonymous) graph; a graph served by name uses its `graphs.<id>.queries`. Mirrors top-level `policy`.
|
||||
<query-name>: { file: <path-to-.gq> } # mcp.expose defaults to true
|
||||
policy:
|
||||
file: ./policy.yaml
|
||||
file: policy.yaml
|
||||
```
|
||||
|
||||
## Cluster config preview
|
||||
|
||||
```bash
|
||||
omnigraph cluster validate --config ./company-brain
|
||||
omnigraph cluster plan --config ./company-brain --json
|
||||
omnigraph cluster apply --config ./company-brain --json
|
||||
omnigraph cluster approve graph.<id> --config ./company-brain --as <actor>
|
||||
omnigraph cluster status --config ./company-brain --json
|
||||
omnigraph cluster refresh --config ./company-brain --json
|
||||
omnigraph cluster import --config ./company-brain --json
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config ./company-brain --json
|
||||
omnigraph cluster validate --config company-brain
|
||||
omnigraph cluster plan --config company-brain --json
|
||||
omnigraph cluster apply --config company-brain --json
|
||||
omnigraph cluster approve graph.<id> --config company-brain --as <actor>
|
||||
omnigraph cluster status --config company-brain --json
|
||||
omnigraph cluster refresh --config company-brain --json
|
||||
omnigraph cluster import --config company-brain --json
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config company-brain --json
|
||||
```
|
||||
|
||||
`--config` is a directory containing `cluster.yaml`; it defaults to `.`.
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
## Core Graph Flow
|
||||
|
||||
```bash
|
||||
omnigraph init --schema ./schema.pg ./graph.omni
|
||||
omnigraph load --data ./data.jsonl --mode overwrite ./graph.omni
|
||||
omnigraph snapshot ./graph.omni --branch main --json
|
||||
omnigraph query --uri ./graph.omni --query ./queries.gq --name get_person --params '{"name":"Alice"}'
|
||||
omnigraph mutate --uri ./graph.omni --query ./queries.gq --name insert_person --params '{"name":"Mina","age":28}'
|
||||
omnigraph init --schema schema.pg graph.omni
|
||||
omnigraph load --data data.jsonl --mode overwrite graph.omni
|
||||
omnigraph snapshot graph.omni --branch main --json
|
||||
omnigraph query --uri graph.omni --query queries.gq --name get_person --params '{"name":"Alice"}'
|
||||
omnigraph mutate --uri graph.omni --query queries.gq --name insert_person --params '{"name":"Mina","age":28}'
|
||||
```
|
||||
|
||||
`omnigraph query` is the canonical read command (pairs with `POST /query`);
|
||||
|
|
@ -21,11 +21,11 @@ For ad-hoc reads and mutations (REPLs, AI agents, one-off scripts), pass the
|
|||
GQ source inline with `-e` / `--query-string` instead of a file path:
|
||||
|
||||
```bash
|
||||
omnigraph query --uri ./graph.omni \
|
||||
omnigraph query --uri graph.omni \
|
||||
-e 'query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }' \
|
||||
--params '{"name":"Alice"}'
|
||||
|
||||
omnigraph mutate --uri ./graph.omni \
|
||||
omnigraph mutate --uri graph.omni \
|
||||
-e 'query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }' \
|
||||
--params '{"name":"Inline","age":42}'
|
||||
```
|
||||
|
|
@ -38,14 +38,14 @@ only the source loader changes.
|
|||
## Branching And Reviewable Data Flows
|
||||
|
||||
```bash
|
||||
omnigraph branch create --uri ./graph.omni --from main feature-x
|
||||
omnigraph branch list --uri ./graph.omni
|
||||
omnigraph branch merge --uri ./graph.omni feature-x --into main
|
||||
omnigraph branch create --uri graph.omni --from main feature-x
|
||||
omnigraph branch list --uri graph.omni
|
||||
omnigraph branch merge --uri graph.omni feature-x --into main
|
||||
|
||||
omnigraph ingest --data ./batch.jsonl --branch review/import-2026-04-09 ./graph.omni
|
||||
omnigraph export ./graph.omni --branch main --type Person > people.jsonl
|
||||
omnigraph commit list ./graph.omni --branch main --json
|
||||
omnigraph commit show --uri ./graph.omni <commit-id> --json
|
||||
omnigraph ingest --data batch.jsonl --branch review/import-2026-04-09 graph.omni
|
||||
omnigraph export graph.omni --branch main --type Person > people.jsonl
|
||||
omnigraph commit list graph.omni --branch main --json
|
||||
omnigraph commit show --uri graph.omni <commit-id> --json
|
||||
```
|
||||
|
||||
## Remote Server Mode
|
||||
|
|
@ -53,7 +53,7 @@ omnigraph commit show --uri ./graph.omni <commit-id> --json
|
|||
Serve a graph:
|
||||
|
||||
```bash
|
||||
omnigraph-server ./graph.omni --bind 127.0.0.1:8080
|
||||
omnigraph-server graph.omni --bind 127.0.0.1:8080
|
||||
```
|
||||
|
||||
Read through the HTTP API:
|
||||
|
|
@ -61,7 +61,7 @@ Read through the HTTP API:
|
|||
```bash
|
||||
omnigraph query \
|
||||
--target http://127.0.0.1:8080 \
|
||||
--query ./queries.gq \
|
||||
--query queries.gq \
|
||||
--name get_person \
|
||||
--params '{"name":"Alice"}'
|
||||
```
|
||||
|
|
@ -87,23 +87,23 @@ Runtime add/remove is **not** in v0.6.0. To add a graph, stop the server, add a
|
|||
Per-graph URLs: hit a graph's cluster route from any subcommand by pointing `--uri` at it:
|
||||
|
||||
```bash
|
||||
omnigraph read --uri http://server.example.com/graphs/beta --query ./q.gq ...
|
||||
omnigraph read --uri http://server.example.com/graphs/beta --query q.gq ...
|
||||
```
|
||||
|
||||
## Runs, Policy, And Diagnostics
|
||||
|
||||
```bash
|
||||
omnigraph lint --query ./queries.gq --schema ./schema.pg --json
|
||||
omnigraph check --query ./queries.gq ./graph.omni --json
|
||||
omnigraph lint --query queries.gq --schema schema.pg --json
|
||||
omnigraph check --query queries.gq graph.omni --json
|
||||
|
||||
omnigraph schema plan --schema ./next.pg ./graph.omni --json
|
||||
omnigraph schema apply --schema ./next.pg ./graph.omni --json
|
||||
omnigraph policy validate --config ./omnigraph.yaml
|
||||
omnigraph policy test --config ./omnigraph.yaml
|
||||
omnigraph policy explain --config ./omnigraph.yaml --actor act-alice --action read --branch main
|
||||
omnigraph schema plan --schema next.pg graph.omni --json
|
||||
omnigraph schema apply --schema next.pg graph.omni --json
|
||||
omnigraph policy validate --config omnigraph.yaml
|
||||
omnigraph policy test --config omnigraph.yaml
|
||||
omnigraph policy explain --config omnigraph.yaml --actor act-alice --action read --branch main
|
||||
|
||||
omnigraph commit list ./graph.omni --json
|
||||
omnigraph commit show --uri ./graph.omni <commit-id> --json
|
||||
omnigraph commit list graph.omni --json
|
||||
omnigraph commit show --uri graph.omni <commit-id> --json
|
||||
```
|
||||
|
||||
(The legacy `omnigraph run list/show/publish/abort` subcommands were removed in MR-771; mutations and loads publish atomically and the commit graph (`omnigraph commit list`) is the audit surface.)
|
||||
|
|
@ -120,7 +120,7 @@ query roots:
|
|||
```yaml
|
||||
graphs:
|
||||
local:
|
||||
uri: ./demo.omni
|
||||
uri: demo.omni
|
||||
dev:
|
||||
uri: http://127.0.0.1:8080
|
||||
bearer_token_env: OMNIGRAPH_BEARER_TOKEN
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ or serve anything it applies: the server still boots from `omnigraph.yaml`.
|
|||
## Commands
|
||||
|
||||
```bash
|
||||
omnigraph cluster validate --config ./company-brain
|
||||
omnigraph cluster plan --config ./company-brain --json
|
||||
omnigraph cluster apply --config ./company-brain --json
|
||||
omnigraph cluster approve graph.<id> --config ./company-brain --as <actor>
|
||||
omnigraph cluster status --config ./company-brain --json
|
||||
omnigraph cluster refresh --config ./company-brain --json
|
||||
omnigraph cluster import --config ./company-brain --json
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config ./company-brain --json
|
||||
omnigraph cluster validate --config company-brain
|
||||
omnigraph cluster plan --config company-brain --json
|
||||
omnigraph cluster apply --config company-brain --json
|
||||
omnigraph cluster approve graph.<id> --config company-brain --as <actor>
|
||||
omnigraph cluster status --config company-brain --json
|
||||
omnigraph cluster refresh --config company-brain --json
|
||||
omnigraph cluster import --config company-brain --json
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config company-brain --json
|
||||
```
|
||||
|
||||
`--config` points at a directory, not a file. The directory must contain
|
||||
|
|
@ -54,7 +54,7 @@ The exact contract:
|
|||
cluster state XOR `omnigraph.yaml`, never a merge.
|
||||
- **The other direction is ergonomics, not coupling**: a per-operator
|
||||
`omnigraph.yaml` may point `graphs.<name>.uri` at a cluster's derived root
|
||||
(`./company-brain/graphs/knowledge.omni`) so data-plane commands can use
|
||||
(`company-brain/graphs/knowledge.omni`) so data-plane commands can use
|
||||
`--target <name>` — an ordinary local path, no special handling.
|
||||
|
||||
## Supported `cluster.yaml`
|
||||
|
|
@ -72,17 +72,35 @@ state:
|
|||
|
||||
graphs:
|
||||
knowledge:
|
||||
schema: ./knowledge.pg
|
||||
queries:
|
||||
find_experts:
|
||||
file: ./knowledge.gq
|
||||
schema: knowledge.pg
|
||||
queries: queries/ # discover every `query <name>` in queries/*.gq
|
||||
|
||||
policies:
|
||||
base:
|
||||
file: ./base.policy.yaml
|
||||
file: base.policy.yaml
|
||||
applies_to: [knowledge]
|
||||
```
|
||||
|
||||
`queries` is Terraform-shaped — the `.gq` files are the declaration. Three
|
||||
forms:
|
||||
|
||||
```yaml
|
||||
queries: queries/ # directory: top-level *.gq, sorted; every declaration registers
|
||||
queries: [people.gq, extra/a.gq] # explicit files; every declaration in each
|
||||
queries: # fine-grained name -> file map
|
||||
find_experts:
|
||||
file: knowledge.gq
|
||||
```
|
||||
|
||||
Discovery is loud: an unreadable or unparseable `.gq`, or the same query name
|
||||
declared in two files, fails validation (`query_parse_error`,
|
||||
`duplicate_query_name`). Each discovered query is still an individually
|
||||
addressed resource (`query.<graph>.<name>`) with its own plan/apply lifecycle;
|
||||
the digest is the containing file's hash, so editing a multi-query file
|
||||
updates all of its queries together. Paths are relative to the config
|
||||
directory — the cluster is one explicit folder, so no `./` prefixes are
|
||||
needed.
|
||||
|
||||
`metadata.name` is a display label. `state.backend` may be omitted or set to
|
||||
`cluster`; external state backends are reserved for a later stage. `state.lock`
|
||||
defaults to `true`. When enabled, `cluster plan`, `cluster apply`,
|
||||
|
|
@ -324,7 +342,7 @@ without graph movement.
|
|||
## Serving from the cluster (the mode switch)
|
||||
|
||||
```bash
|
||||
omnigraph-server --cluster ./company-brain --bind 0.0.0.0:8080
|
||||
omnigraph-server --cluster company-brain --bind 0.0.0.0:8080
|
||||
```
|
||||
|
||||
`--cluster <dir>` is an **exclusive boot source** (axiom 15): it cannot
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ Lay out a config directory:
|
|||
company-brain/
|
||||
├── cluster.yaml
|
||||
├── people.pg # schema for the "knowledge" graph
|
||||
├── people.gq # a stored query
|
||||
├── queries/ # stored queries — the .gq files ARE the declaration
|
||||
│ └── people.gq
|
||||
└── base.policy.yaml # a Cedar policy bundle
|
||||
```
|
||||
|
||||
|
|
@ -43,27 +44,25 @@ metadata:
|
|||
name: company-brain
|
||||
graphs:
|
||||
knowledge:
|
||||
schema: ./people.pg
|
||||
queries:
|
||||
find_person:
|
||||
file: ./people.gq
|
||||
schema: people.pg
|
||||
queries: queries/ # every `query <name>` in queries/*.gq registers
|
||||
policies:
|
||||
base:
|
||||
file: ./base.policy.yaml
|
||||
file: base.policy.yaml
|
||||
applies_to: [knowledge] # graph-bound; use [cluster] for server-level
|
||||
```
|
||||
|
||||
Bring it to life:
|
||||
|
||||
```bash
|
||||
omnigraph cluster validate --config ./company-brain # parse + typecheck everything
|
||||
omnigraph cluster import --config ./company-brain # create the state ledger
|
||||
omnigraph cluster plan --config ./company-brain # preview: what would apply do?
|
||||
omnigraph cluster apply --config ./company-brain # converge
|
||||
omnigraph cluster validate --config company-brain # parse + typecheck everything
|
||||
omnigraph cluster import --config company-brain # create the state ledger
|
||||
omnigraph cluster plan --config company-brain # preview: what would apply do?
|
||||
omnigraph cluster apply --config company-brain # converge
|
||||
```
|
||||
|
||||
That single `apply` **creates the graph** (at the derived root
|
||||
`./company-brain/graphs/knowledge.omni`), applies its schema, and publishes
|
||||
`company-brain/graphs/knowledge.omni`), applies its schema, and publishes
|
||||
the query and policy into the content-addressed catalog
|
||||
(`__cluster/resources/…`). The output lists every change with its
|
||||
disposition; `converged: true` means there is nothing left to do — re-running
|
||||
|
|
@ -73,14 +72,14 @@ Load data through the normal graph plane (the control plane manages
|
|||
*definitions*, not rows):
|
||||
|
||||
```bash
|
||||
omnigraph load --data ./seed.jsonl ./company-brain/graphs/knowledge.omni
|
||||
omnigraph load --data seed.jsonl company-brain/graphs/knowledge.omni
|
||||
```
|
||||
|
||||
Serve it:
|
||||
|
||||
```bash
|
||||
OMNIGRAPH_SERVER_BEARER_TOKENS_JSON='{"act-reader":"s3cret"}' \
|
||||
omnigraph-server --cluster ./company-brain --bind 0.0.0.0:8080
|
||||
omnigraph-server --cluster company-brain --bind 0.0.0.0:8080
|
||||
```
|
||||
|
||||
`--cluster` is an **exclusive boot source**: it cannot be combined with a
|
||||
|
|
@ -103,8 +102,8 @@ Every change follows the same loop, whatever its kind:
|
|||
|
||||
```bash
|
||||
$EDITOR company-brain/people.pg # or any .gq / policy / cluster.yaml edit
|
||||
omnigraph cluster plan --config ./company-brain
|
||||
omnigraph cluster apply --config ./company-brain --as andrew
|
||||
omnigraph cluster plan --config company-brain
|
||||
omnigraph cluster apply --config company-brain --as andrew
|
||||
# restart cluster-booted servers to pick it up
|
||||
```
|
||||
|
||||
|
|
@ -142,8 +141,8 @@ anything runs.
|
|||
## 3. Inspect: status, refresh, drift
|
||||
|
||||
```bash
|
||||
omnigraph cluster status --config ./company-brain --json # ledger only, read-only
|
||||
omnigraph cluster refresh --config ./company-brain # re-observe live graphs
|
||||
omnigraph cluster status --config company-brain --json # ledger only, read-only
|
||||
omnigraph cluster refresh --config company-brain # re-observe live graphs
|
||||
```
|
||||
|
||||
`status` never touches the graphs; `refresh` opens them read-only and
|
||||
|
|
@ -164,13 +163,13 @@ converges the ledger.
|
|||
Removing a graph from `cluster.yaml` never executes silently:
|
||||
|
||||
```bash
|
||||
omnigraph cluster apply --config ./company-brain
|
||||
omnigraph cluster apply --config company-brain
|
||||
# Delete graph.scratch [Blocked: approval_required]
|
||||
|
||||
omnigraph cluster approve graph.scratch --config ./company-brain --as andrew
|
||||
omnigraph cluster approve graph.scratch --config company-brain --as andrew
|
||||
# cluster approve: delete graph.scratch approved by andrew (approval 01KT…)
|
||||
|
||||
omnigraph cluster apply --config ./company-brain --as andrew
|
||||
omnigraph cluster apply --config company-brain --as andrew
|
||||
# Delete graph.scratch [Applied] ← root removed, subtree tombstoned
|
||||
```
|
||||
|
||||
|
|
@ -196,8 +195,8 @@ again**.
|
|||
**A held lock** (a crashed process left `__cluster/lock.json`):
|
||||
|
||||
```bash
|
||||
omnigraph cluster status --config ./company-brain # shows the lock holder + id
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config ./company-brain
|
||||
omnigraph cluster status --config company-brain # shows the lock holder + id
|
||||
omnigraph cluster force-unlock <LOCK_ID> --config company-brain
|
||||
```
|
||||
|
||||
Force-unlock requires the exact lock id (from status) — there is no blind
|
||||
|
|
@ -240,7 +239,7 @@ with an in-flight apply.
|
|||
- **`omnigraph.yaml` still has a job**: per-operator settings — your
|
||||
`cli.actor` default for `--as`, CLI defaults, credentials, and data-plane
|
||||
ergonomics (point `graphs.<name>.uri` at a derived root like
|
||||
`./company-brain/graphs/knowledge.omni` to use `--target <name>` for
|
||||
`company-brain/graphs/knowledge.omni` to use `--target <name>` for
|
||||
loads). It just no longer describes the deployment — a server boots from
|
||||
one source or the other, never a merge of both.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ On Windows, the binaries are `omnigraph.exe` and `omnigraph-server.exe`.
|
|||
Run against a local graph:
|
||||
|
||||
```bash
|
||||
omnigraph-server ./graph.omni --bind 0.0.0.0:8080
|
||||
omnigraph-server graph.omni --bind 0.0.0.0:8080
|
||||
```
|
||||
|
||||
Run against an object-store-backed graph:
|
||||
|
|
@ -208,7 +208,7 @@ docker run --rm -p 8080:8080 \
|
|||
-e OMNIGRAPH_CONFIG="/etc/omnigraph/omnigraph.yaml" \
|
||||
-v "$PWD/config:/etc/omnigraph:ro" \
|
||||
omnigraph-server:local
|
||||
# /etc/omnigraph/omnigraph.yaml contains `policy: { file: ./policy.yaml }`;
|
||||
# /etc/omnigraph/omnigraph.yaml contains `policy: { file: policy.yaml }`;
|
||||
# policy.yaml (+ optional policy.tests.yaml) sit beside it in the mount.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@ In multi mode (`omnigraph.yaml` with a non-empty `graphs:` map), policy files at
|
|||
```yaml
|
||||
server:
|
||||
policy:
|
||||
file: ./server-policy.yaml # server-level: graph_list
|
||||
file: server-policy.yaml # server-level: graph_list
|
||||
|
||||
graphs:
|
||||
alpha:
|
||||
uri: s3://tenant-bucket/alpha
|
||||
policy:
|
||||
file: ./policies/alpha.yaml # per-graph: read, change, branch_*, schema_apply
|
||||
file: policies/alpha.yaml # per-graph: read, change, branch_*, schema_apply
|
||||
beta:
|
||||
uri: s3://tenant-bucket/beta
|
||||
# no per-graph policy → no engine-layer Cedar enforcement on beta
|
||||
|
|
@ -78,8 +78,8 @@ rules:
|
|||
|
||||
```yaml
|
||||
policy:
|
||||
file: ./policy.yaml # Cedar rules + groups
|
||||
tests: ./policy.tests.yaml # declarative test cases
|
||||
file: policy.yaml # Cedar rules + groups
|
||||
tests: policy.tests.yaml # declarative test cases
|
||||
|
||||
cli:
|
||||
actor: act-andrew # default actor for CLI direct-engine writes
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ query register_employee_with_team($name: String, $age: I32, $team: String) {
|
|||
```
|
||||
|
||||
```bash
|
||||
omnigraph change --query ./mutations.gq --name register_employee_with_team \
|
||||
--params '{"name":"Alice","age":30,"team":"Acme"}' ./graph.omni
|
||||
omnigraph change --query mutations.gq --name register_employee_with_team \
|
||||
--params '{"name":"Alice","age":30,"team":"Acme"}' graph.omni
|
||||
```
|
||||
|
||||
If the second statement fails (e.g. `Acme` doesn't exist), the publisher never publishes; `Alice` is not in the database. Atomic.
|
||||
|
|
@ -57,10 +57,10 @@ If the second statement fails (e.g. `Acme` doesn't exist), the publisher never p
|
|||
|
||||
```bash
|
||||
# Query 1
|
||||
omnigraph change --query ./mutations.gq --name register_employee --params '{"name":"Alice","age":30}' ./graph.omni
|
||||
omnigraph change --query mutations.gq --name register_employee --params '{"name":"Alice","age":30}' graph.omni
|
||||
|
||||
# Query 2 — runs after Query 1 has already published
|
||||
omnigraph change --query ./mutations.gq --name link_to_team --params '{"name":"Alice","team":"Acme"}' ./graph.omni
|
||||
omnigraph change --query mutations.gq --name link_to_team --params '{"name":"Alice","team":"Acme"}' graph.omni
|
||||
```
|
||||
|
||||
These are **two publishes** on `main`. If Query 2 fails, Query 1's effects are already visible. There is no `ROLLBACK` for Query 1.
|
||||
|
|
@ -75,32 +75,32 @@ The pattern when you need to run multiple queries — possibly across multiple c
|
|||
|
||||
```bash
|
||||
# Fork a working branch from main.
|
||||
omnigraph branch create --from main onboarding/2026-04-25 ./graph.omni
|
||||
omnigraph branch create --from main onboarding/2026-04-25 graph.omni
|
||||
|
||||
# Run any number of mutations on the branch — each one is its own publish on the branch.
|
||||
# Concurrent reads of `main` are unaffected.
|
||||
omnigraph change --branch onboarding/2026-04-25 \
|
||||
--query ./mutations.gq --name register_employee \
|
||||
--params '{"name":"Alice","age":30}' ./graph.omni
|
||||
--query mutations.gq --name register_employee \
|
||||
--params '{"name":"Alice","age":30}' graph.omni
|
||||
|
||||
omnigraph change --branch onboarding/2026-04-25 \
|
||||
--query ./mutations.gq --name register_employee \
|
||||
--params '{"name":"Bob","age":25}' ./graph.omni
|
||||
--query mutations.gq --name register_employee \
|
||||
--params '{"name":"Bob","age":25}' graph.omni
|
||||
|
||||
omnigraph change --branch onboarding/2026-04-25 \
|
||||
--query ./mutations.gq --name link_to_team \
|
||||
--params '{"name":"Alice","team":"Acme"}' ./graph.omni
|
||||
--query mutations.gq --name link_to_team \
|
||||
--params '{"name":"Alice","team":"Acme"}' graph.omni
|
||||
|
||||
# Inspect the branch — read queries work just like on main.
|
||||
omnigraph read --branch onboarding/2026-04-25 \
|
||||
--query ./queries.gq --name list_employees ./graph.omni
|
||||
--query queries.gq --name list_employees graph.omni
|
||||
|
||||
# Happy with what's on the branch? Merge it. This is one atomic publish:
|
||||
# `main` flips to include every commit on the branch.
|
||||
omnigraph branch merge onboarding/2026-04-25 --into main ./graph.omni
|
||||
omnigraph branch merge onboarding/2026-04-25 --into main graph.omni
|
||||
|
||||
# OR: not happy? Throw it away. `main` is untouched.
|
||||
# omnigraph branch delete onboarding/2026-04-25 ./graph.omni
|
||||
# omnigraph branch delete onboarding/2026-04-25 graph.omni
|
||||
```
|
||||
|
||||
Properties:
|
||||
|
|
@ -115,16 +115,16 @@ Two agents writing to the same graph independently:
|
|||
|
||||
```bash
|
||||
# Agent A
|
||||
omnigraph branch create --from main agent-a/work ./graph.omni
|
||||
omnigraph change --branch agent-a/work … ./graph.omni
|
||||
omnigraph branch create --from main agent-a/work graph.omni
|
||||
omnigraph change --branch agent-a/work … graph.omni
|
||||
# … many mutations …
|
||||
omnigraph branch merge agent-a/work --into main ./graph.omni
|
||||
omnigraph branch merge agent-a/work --into main graph.omni
|
||||
|
||||
# Agent B (running concurrently)
|
||||
omnigraph branch create --from main agent-b/work ./graph.omni
|
||||
omnigraph change --branch agent-b/work … ./graph.omni
|
||||
omnigraph branch create --from main agent-b/work graph.omni
|
||||
omnigraph change --branch agent-b/work … graph.omni
|
||||
# … many mutations …
|
||||
omnigraph branch merge agent-b/work --into main ./graph.omni
|
||||
omnigraph branch merge agent-b/work --into main graph.omni
|
||||
```
|
||||
|
||||
Each agent sees a consistent snapshot of `main` at the time it forked. The first merge to `main` lands as a fast-forward (or a no-op if no concurrent change). The second merge runs three-way: rows touched by both branches surface as `MergeConflict`s for the caller to resolve.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue