mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-07-06 02:52:11 +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 {
|
struct GraphConfig {
|
||||||
schema: PathBuf,
|
schema: PathBuf,
|
||||||
#[serde(default)]
|
#[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)]
|
#[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(
|
validate_id(
|
||||||
"query name",
|
"query name",
|
||||||
&format!("graphs.{graph_id}.queries.{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);
|
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) => {
|
Ok(source) => {
|
||||||
let digest = sha256_hex(source.as_bytes());
|
let digest = sha256_hex(source.as_bytes());
|
||||||
graph_query_digests
|
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]
|
#[test]
|
||||||
fn status_warns_on_pending_recovery_sidecar() {
|
fn status_warns_on_pending_recovery_sidecar() {
|
||||||
let dir = fixture();
|
let dir = fixture();
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ cli:
|
||||||
query:
|
query:
|
||||||
roots: [<dir>, …] # search path for .gq files
|
roots: [<dir>, …] # search path for .gq files
|
||||||
auth:
|
auth:
|
||||||
env_file: ./.env.omni
|
env_file: .env.omni
|
||||||
aliases:
|
aliases:
|
||||||
<alias>:
|
<alias>:
|
||||||
# accepted values: `read` / `query` (read alias), `change` / `mutate`
|
# 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`.
|
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
|
<query-name>: { file: <path-to-.gq> } # mcp.expose defaults to true
|
||||||
policy:
|
policy:
|
||||||
file: ./policy.yaml
|
file: policy.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cluster config preview
|
## Cluster config preview
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster validate --config ./company-brain
|
omnigraph cluster validate --config company-brain
|
||||||
omnigraph cluster plan --config ./company-brain --json
|
omnigraph cluster plan --config company-brain --json
|
||||||
omnigraph cluster apply --config ./company-brain --json
|
omnigraph cluster apply --config company-brain --json
|
||||||
omnigraph cluster approve graph.<id> --config ./company-brain --as <actor>
|
omnigraph cluster approve graph.<id> --config company-brain --as <actor>
|
||||||
omnigraph cluster status --config ./company-brain --json
|
omnigraph cluster status --config company-brain --json
|
||||||
omnigraph cluster refresh --config ./company-brain --json
|
omnigraph cluster refresh --config company-brain --json
|
||||||
omnigraph cluster import --config ./company-brain --json
|
omnigraph cluster import --config company-brain --json
|
||||||
omnigraph cluster force-unlock <LOCK_ID> --config ./company-brain --json
|
omnigraph cluster force-unlock <LOCK_ID> --config company-brain --json
|
||||||
```
|
```
|
||||||
|
|
||||||
`--config` is a directory containing `cluster.yaml`; it defaults to `.`.
|
`--config` is a directory containing `cluster.yaml`; it defaults to `.`.
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
## Core Graph Flow
|
## Core Graph Flow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph init --schema ./schema.pg ./graph.omni
|
omnigraph init --schema schema.pg graph.omni
|
||||||
omnigraph load --data ./data.jsonl --mode overwrite ./graph.omni
|
omnigraph load --data data.jsonl --mode overwrite graph.omni
|
||||||
omnigraph snapshot ./graph.omni --branch main --json
|
omnigraph snapshot graph.omni --branch main --json
|
||||||
omnigraph query --uri ./graph.omni --query ./queries.gq --name get_person --params '{"name":"Alice"}'
|
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 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`);
|
`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:
|
GQ source inline with `-e` / `--query-string` instead of a file path:
|
||||||
|
|
||||||
```bash
|
```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 } }' \
|
-e 'query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }' \
|
||||||
--params '{"name":"Alice"}'
|
--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 } }' \
|
-e 'query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }' \
|
||||||
--params '{"name":"Inline","age":42}'
|
--params '{"name":"Inline","age":42}'
|
||||||
```
|
```
|
||||||
|
|
@ -38,14 +38,14 @@ only the source loader changes.
|
||||||
## Branching And Reviewable Data Flows
|
## Branching And Reviewable Data Flows
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph branch create --uri ./graph.omni --from main feature-x
|
omnigraph branch create --uri graph.omni --from main feature-x
|
||||||
omnigraph branch list --uri ./graph.omni
|
omnigraph branch list --uri graph.omni
|
||||||
omnigraph branch merge --uri ./graph.omni feature-x --into main
|
omnigraph branch merge --uri graph.omni feature-x --into main
|
||||||
|
|
||||||
omnigraph ingest --data ./batch.jsonl --branch review/import-2026-04-09 ./graph.omni
|
omnigraph ingest --data batch.jsonl --branch review/import-2026-04-09 graph.omni
|
||||||
omnigraph export ./graph.omni --branch main --type Person > people.jsonl
|
omnigraph export graph.omni --branch main --type Person > people.jsonl
|
||||||
omnigraph commit list ./graph.omni --branch main --json
|
omnigraph commit list graph.omni --branch main --json
|
||||||
omnigraph commit show --uri ./graph.omni <commit-id> --json
|
omnigraph commit show --uri graph.omni <commit-id> --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Remote Server Mode
|
## Remote Server Mode
|
||||||
|
|
@ -53,7 +53,7 @@ omnigraph commit show --uri ./graph.omni <commit-id> --json
|
||||||
Serve a graph:
|
Serve a graph:
|
||||||
|
|
||||||
```bash
|
```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:
|
Read through the HTTP API:
|
||||||
|
|
@ -61,7 +61,7 @@ Read through the HTTP API:
|
||||||
```bash
|
```bash
|
||||||
omnigraph query \
|
omnigraph query \
|
||||||
--target http://127.0.0.1:8080 \
|
--target http://127.0.0.1:8080 \
|
||||||
--query ./queries.gq \
|
--query queries.gq \
|
||||||
--name get_person \
|
--name get_person \
|
||||||
--params '{"name":"Alice"}'
|
--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:
|
Per-graph URLs: hit a graph's cluster route from any subcommand by pointing `--uri` at it:
|
||||||
|
|
||||||
```bash
|
```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
|
## Runs, Policy, And Diagnostics
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph lint --query ./queries.gq --schema ./schema.pg --json
|
omnigraph lint --query queries.gq --schema schema.pg --json
|
||||||
omnigraph check --query ./queries.gq ./graph.omni --json
|
omnigraph check --query queries.gq graph.omni --json
|
||||||
|
|
||||||
omnigraph schema plan --schema ./next.pg ./graph.omni --json
|
omnigraph schema plan --schema next.pg graph.omni --json
|
||||||
omnigraph schema apply --schema ./next.pg ./graph.omni --json
|
omnigraph schema apply --schema next.pg graph.omni --json
|
||||||
omnigraph policy validate --config ./omnigraph.yaml
|
omnigraph policy validate --config omnigraph.yaml
|
||||||
omnigraph policy test --config ./omnigraph.yaml
|
omnigraph policy test --config omnigraph.yaml
|
||||||
omnigraph policy explain --config ./omnigraph.yaml --actor act-alice --action read --branch main
|
omnigraph policy explain --config omnigraph.yaml --actor act-alice --action read --branch main
|
||||||
|
|
||||||
omnigraph commit list ./graph.omni --json
|
omnigraph commit list graph.omni --json
|
||||||
omnigraph commit show --uri ./graph.omni <commit-id> --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.)
|
(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
|
```yaml
|
||||||
graphs:
|
graphs:
|
||||||
local:
|
local:
|
||||||
uri: ./demo.omni
|
uri: demo.omni
|
||||||
dev:
|
dev:
|
||||||
uri: http://127.0.0.1:8080
|
uri: http://127.0.0.1:8080
|
||||||
bearer_token_env: OMNIGRAPH_BEARER_TOKEN
|
bearer_token_env: OMNIGRAPH_BEARER_TOKEN
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@ or serve anything it applies: the server still boots from `omnigraph.yaml`.
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster validate --config ./company-brain
|
omnigraph cluster validate --config company-brain
|
||||||
omnigraph cluster plan --config ./company-brain --json
|
omnigraph cluster plan --config company-brain --json
|
||||||
omnigraph cluster apply --config ./company-brain --json
|
omnigraph cluster apply --config company-brain --json
|
||||||
omnigraph cluster approve graph.<id> --config ./company-brain --as <actor>
|
omnigraph cluster approve graph.<id> --config company-brain --as <actor>
|
||||||
omnigraph cluster status --config ./company-brain --json
|
omnigraph cluster status --config company-brain --json
|
||||||
omnigraph cluster refresh --config ./company-brain --json
|
omnigraph cluster refresh --config company-brain --json
|
||||||
omnigraph cluster import --config ./company-brain --json
|
omnigraph cluster import --config company-brain --json
|
||||||
omnigraph cluster force-unlock <LOCK_ID> --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
|
`--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.
|
cluster state XOR `omnigraph.yaml`, never a merge.
|
||||||
- **The other direction is ergonomics, not coupling**: a per-operator
|
- **The other direction is ergonomics, not coupling**: a per-operator
|
||||||
`omnigraph.yaml` may point `graphs.<name>.uri` at a cluster's derived root
|
`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.
|
`--target <name>` — an ordinary local path, no special handling.
|
||||||
|
|
||||||
## Supported `cluster.yaml`
|
## Supported `cluster.yaml`
|
||||||
|
|
@ -72,17 +72,35 @@ state:
|
||||||
|
|
||||||
graphs:
|
graphs:
|
||||||
knowledge:
|
knowledge:
|
||||||
schema: ./knowledge.pg
|
schema: knowledge.pg
|
||||||
queries:
|
queries: queries/ # discover every `query <name>` in queries/*.gq
|
||||||
find_experts:
|
|
||||||
file: ./knowledge.gq
|
|
||||||
|
|
||||||
policies:
|
policies:
|
||||||
base:
|
base:
|
||||||
file: ./base.policy.yaml
|
file: base.policy.yaml
|
||||||
applies_to: [knowledge]
|
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
|
`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`
|
`cluster`; external state backends are reserved for a later stage. `state.lock`
|
||||||
defaults to `true`. When enabled, `cluster plan`, `cluster apply`,
|
defaults to `true`. When enabled, `cluster plan`, `cluster apply`,
|
||||||
|
|
@ -324,7 +342,7 @@ without graph movement.
|
||||||
## Serving from the cluster (the mode switch)
|
## Serving from the cluster (the mode switch)
|
||||||
|
|
||||||
```bash
|
```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
|
`--cluster <dir>` is an **exclusive boot source** (axiom 15): it cannot
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ Lay out a config directory:
|
||||||
company-brain/
|
company-brain/
|
||||||
├── cluster.yaml
|
├── cluster.yaml
|
||||||
├── people.pg # schema for the "knowledge" graph
|
├── 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
|
└── base.policy.yaml # a Cedar policy bundle
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -43,27 +44,25 @@ metadata:
|
||||||
name: company-brain
|
name: company-brain
|
||||||
graphs:
|
graphs:
|
||||||
knowledge:
|
knowledge:
|
||||||
schema: ./people.pg
|
schema: people.pg
|
||||||
queries:
|
queries: queries/ # every `query <name>` in queries/*.gq registers
|
||||||
find_person:
|
|
||||||
file: ./people.gq
|
|
||||||
policies:
|
policies:
|
||||||
base:
|
base:
|
||||||
file: ./base.policy.yaml
|
file: base.policy.yaml
|
||||||
applies_to: [knowledge] # graph-bound; use [cluster] for server-level
|
applies_to: [knowledge] # graph-bound; use [cluster] for server-level
|
||||||
```
|
```
|
||||||
|
|
||||||
Bring it to life:
|
Bring it to life:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster validate --config ./company-brain # parse + typecheck everything
|
omnigraph cluster validate --config company-brain # parse + typecheck everything
|
||||||
omnigraph cluster import --config ./company-brain # create the state ledger
|
omnigraph cluster import --config company-brain # create the state ledger
|
||||||
omnigraph cluster plan --config ./company-brain # preview: what would apply do?
|
omnigraph cluster plan --config company-brain # preview: what would apply do?
|
||||||
omnigraph cluster apply --config ./company-brain # converge
|
omnigraph cluster apply --config company-brain # converge
|
||||||
```
|
```
|
||||||
|
|
||||||
That single `apply` **creates the graph** (at the derived root
|
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
|
the query and policy into the content-addressed catalog
|
||||||
(`__cluster/resources/…`). The output lists every change with its
|
(`__cluster/resources/…`). The output lists every change with its
|
||||||
disposition; `converged: true` means there is nothing left to do — re-running
|
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):
|
*definitions*, not rows):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph load --data ./seed.jsonl ./company-brain/graphs/knowledge.omni
|
omnigraph load --data seed.jsonl company-brain/graphs/knowledge.omni
|
||||||
```
|
```
|
||||||
|
|
||||||
Serve it:
|
Serve it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OMNIGRAPH_SERVER_BEARER_TOKENS_JSON='{"act-reader":"s3cret"}' \
|
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
|
`--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
|
```bash
|
||||||
$EDITOR company-brain/people.pg # or any .gq / policy / cluster.yaml edit
|
$EDITOR company-brain/people.pg # or any .gq / policy / cluster.yaml edit
|
||||||
omnigraph cluster plan --config ./company-brain
|
omnigraph cluster plan --config company-brain
|
||||||
omnigraph cluster apply --config ./company-brain --as andrew
|
omnigraph cluster apply --config company-brain --as andrew
|
||||||
# restart cluster-booted servers to pick it up
|
# restart cluster-booted servers to pick it up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -142,8 +141,8 @@ anything runs.
|
||||||
## 3. Inspect: status, refresh, drift
|
## 3. Inspect: status, refresh, drift
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster status --config ./company-brain --json # ledger only, read-only
|
omnigraph cluster status --config company-brain --json # ledger only, read-only
|
||||||
omnigraph cluster refresh --config ./company-brain # re-observe live graphs
|
omnigraph cluster refresh --config company-brain # re-observe live graphs
|
||||||
```
|
```
|
||||||
|
|
||||||
`status` never touches the graphs; `refresh` opens them read-only and
|
`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:
|
Removing a graph from `cluster.yaml` never executes silently:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster apply --config ./company-brain
|
omnigraph cluster apply --config company-brain
|
||||||
# Delete graph.scratch [Blocked: approval_required]
|
# 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…)
|
# 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
|
# Delete graph.scratch [Applied] ← root removed, subtree tombstoned
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -196,8 +195,8 @@ again**.
|
||||||
**A held lock** (a crashed process left `__cluster/lock.json`):
|
**A held lock** (a crashed process left `__cluster/lock.json`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omnigraph cluster status --config ./company-brain # shows the lock holder + id
|
omnigraph cluster status --config company-brain # shows the lock holder + id
|
||||||
omnigraph cluster force-unlock <LOCK_ID> --config ./company-brain
|
omnigraph cluster force-unlock <LOCK_ID> --config company-brain
|
||||||
```
|
```
|
||||||
|
|
||||||
Force-unlock requires the exact lock id (from status) — there is no blind
|
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
|
- **`omnigraph.yaml` still has a job**: per-operator settings — your
|
||||||
`cli.actor` default for `--as`, CLI defaults, credentials, and data-plane
|
`cli.actor` default for `--as`, CLI defaults, credentials, and data-plane
|
||||||
ergonomics (point `graphs.<name>.uri` at a derived root like
|
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
|
loads). It just no longer describes the deployment — a server boots from
|
||||||
one source or the other, never a merge of both.
|
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:
|
Run against a local graph:
|
||||||
|
|
||||||
```bash
|
```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:
|
Run against an object-store-backed graph:
|
||||||
|
|
@ -208,7 +208,7 @@ docker run --rm -p 8080:8080 \
|
||||||
-e OMNIGRAPH_CONFIG="/etc/omnigraph/omnigraph.yaml" \
|
-e OMNIGRAPH_CONFIG="/etc/omnigraph/omnigraph.yaml" \
|
||||||
-v "$PWD/config:/etc/omnigraph:ro" \
|
-v "$PWD/config:/etc/omnigraph:ro" \
|
||||||
omnigraph-server:local
|
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.
|
# 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
|
```yaml
|
||||||
server:
|
server:
|
||||||
policy:
|
policy:
|
||||||
file: ./server-policy.yaml # server-level: graph_list
|
file: server-policy.yaml # server-level: graph_list
|
||||||
|
|
||||||
graphs:
|
graphs:
|
||||||
alpha:
|
alpha:
|
||||||
uri: s3://tenant-bucket/alpha
|
uri: s3://tenant-bucket/alpha
|
||||||
policy:
|
policy:
|
||||||
file: ./policies/alpha.yaml # per-graph: read, change, branch_*, schema_apply
|
file: policies/alpha.yaml # per-graph: read, change, branch_*, schema_apply
|
||||||
beta:
|
beta:
|
||||||
uri: s3://tenant-bucket/beta
|
uri: s3://tenant-bucket/beta
|
||||||
# no per-graph policy → no engine-layer Cedar enforcement on beta
|
# no per-graph policy → no engine-layer Cedar enforcement on beta
|
||||||
|
|
@ -78,8 +78,8 @@ rules:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
policy:
|
policy:
|
||||||
file: ./policy.yaml # Cedar rules + groups
|
file: policy.yaml # Cedar rules + groups
|
||||||
tests: ./policy.tests.yaml # declarative test cases
|
tests: policy.tests.yaml # declarative test cases
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
actor: act-andrew # default actor for CLI direct-engine writes
|
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
|
```bash
|
||||||
omnigraph change --query ./mutations.gq --name register_employee_with_team \
|
omnigraph change --query mutations.gq --name register_employee_with_team \
|
||||||
--params '{"name":"Alice","age":30,"team":"Acme"}' ./graph.omni
|
--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.
|
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
|
```bash
|
||||||
# Query 1
|
# 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
|
# 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.
|
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
|
```bash
|
||||||
# Fork a working branch from main.
|
# 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.
|
# Run any number of mutations on the branch — each one is its own publish on the branch.
|
||||||
# Concurrent reads of `main` are unaffected.
|
# Concurrent reads of `main` are unaffected.
|
||||||
omnigraph change --branch onboarding/2026-04-25 \
|
omnigraph change --branch onboarding/2026-04-25 \
|
||||||
--query ./mutations.gq --name register_employee \
|
--query mutations.gq --name register_employee \
|
||||||
--params '{"name":"Alice","age":30}' ./graph.omni
|
--params '{"name":"Alice","age":30}' graph.omni
|
||||||
|
|
||||||
omnigraph change --branch onboarding/2026-04-25 \
|
omnigraph change --branch onboarding/2026-04-25 \
|
||||||
--query ./mutations.gq --name register_employee \
|
--query mutations.gq --name register_employee \
|
||||||
--params '{"name":"Bob","age":25}' ./graph.omni
|
--params '{"name":"Bob","age":25}' graph.omni
|
||||||
|
|
||||||
omnigraph change --branch onboarding/2026-04-25 \
|
omnigraph change --branch onboarding/2026-04-25 \
|
||||||
--query ./mutations.gq --name link_to_team \
|
--query mutations.gq --name link_to_team \
|
||||||
--params '{"name":"Alice","team":"Acme"}' ./graph.omni
|
--params '{"name":"Alice","team":"Acme"}' graph.omni
|
||||||
|
|
||||||
# Inspect the branch — read queries work just like on main.
|
# Inspect the branch — read queries work just like on main.
|
||||||
omnigraph read --branch onboarding/2026-04-25 \
|
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:
|
# Happy with what's on the branch? Merge it. This is one atomic publish:
|
||||||
# `main` flips to include every commit on the branch.
|
# `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.
|
# 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:
|
Properties:
|
||||||
|
|
@ -115,16 +115,16 @@ Two agents writing to the same graph independently:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Agent A
|
# Agent A
|
||||||
omnigraph branch create --from main agent-a/work ./graph.omni
|
omnigraph branch create --from main agent-a/work graph.omni
|
||||||
omnigraph change --branch agent-a/work … ./graph.omni
|
omnigraph change --branch agent-a/work … graph.omni
|
||||||
# … many mutations …
|
# … 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)
|
# Agent B (running concurrently)
|
||||||
omnigraph branch create --from main agent-b/work ./graph.omni
|
omnigraph branch create --from main agent-b/work graph.omni
|
||||||
omnigraph change --branch agent-b/work … ./graph.omni
|
omnigraph change --branch agent-b/work … graph.omni
|
||||||
# … many mutations …
|
# … 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.
|
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