feat(cli)!: unified load command; deprecate ingest as an alias

omnigraph load is now the single data-write command:
- works against remote graphs (POSTs the server's /ingest endpoint with the
  same bearer/actor resolution as other remote commands) — previously load
  was the only data command forced to open Lance storage directly
- --from <base> opts into fork-if-missing for --branch (the former ingest
  semantics); without --from a missing branch is an error, never a fork
- --mode is now required: overwrite is destructive, so there is no implicit
  default (the old silent default was overwrite)
- output gains base_branch/branch_created (and table sums on remote loads)

omnigraph ingest stays as a deprecated alias (defaults preserved: --from
main --mode merge) that prints a one-line warning to stderr, matching the
read/change deprecation convention; removal in a later release.

Docs updated in the same change: cli.md, cli-reference.md, policy.md,
audit.md, execution.md (unified load section), AGENTS.md quick-flow,
README.md.

BREAKING CHANGE: scripts running omnigraph load without --mode must now
pass it explicitly (previously defaulted to the destructive overwrite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-06-11 04:18:00 +03:00
parent 90676ef52f
commit fa6af775c1
12 changed files with 342 additions and 68 deletions

View file

@ -2650,6 +2650,8 @@ fn load_json_outputs_summary_for_main_branch() {
let output = output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg("--json")
@ -2984,7 +2986,15 @@ fn read_alias_uses_alias_target_without_cli_default_and_accepts_url_like_arg() {
&data,
r#"{"type":"Person","data":{"name":"https://example.com","age":30}}"#,
);
output_success(cli().arg("load").arg("--data").arg(&data).arg(&graph));
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(&graph),
);
write_query_file(
&query,
&std::fs::read_to_string(fixture("test.gq")).unwrap(),
@ -3748,6 +3758,8 @@ fn cli_fails_for_missing_schema_or_data_file() {
let load_output = output_failure(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&missing_data)
.arg(&graph),

View file

@ -93,7 +93,15 @@ pub fn init_graph(graph: &Path) {
pub fn load_fixture(graph: &Path) {
let data = fixture("test.jsonl");
output_success(cli().arg("load").arg("--data").arg(&data).arg(graph));
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(graph),
);
}
pub fn write_jsonl(path: &Path, rows: &str) {

View file

@ -221,6 +221,8 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(fixture("test.jsonl"))
.arg(graph.path()),
@ -397,7 +399,7 @@ fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
{"type":"Person","data":{"name":"Bob","age":26}}"#,
);
let ingest_payload = parse_stdout_json(&output_success(
let ingest_output = output_success(
cli()
.arg("ingest")
.arg("--data")
@ -406,7 +408,13 @@ fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
.arg("feature-ingest")
.arg(graph.path())
.arg("--json"),
));
);
// The deprecation warning goes to stderr so --json stdout stays clean.
assert!(
String::from_utf8_lossy(&ingest_output.stderr).contains("deprecated"),
"ingest must warn about its deprecation on stderr"
);
let ingest_payload = parse_stdout_json(&ingest_output);
assert_eq!(ingest_payload["branch"], "feature-ingest");
assert_eq!(ingest_payload["base_branch"], "main");
assert_eq!(ingest_payload["branch_created"], true);
@ -459,6 +467,88 @@ fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
assert_eq!(bob["rows"][0]["p.age"], 26);
}
/// The unified `load` subsumes ingest: `--from` opts into fork-if-missing,
/// while without it a missing branch is an error — never an implicit fork.
#[test]
fn local_cli_load_from_forks_branch_and_missing_branch_errors_without_from() {
let graph = SystemGraph::loaded();
let extra = graph.write_jsonl(
"system-local-load-from.jsonl",
r#"{"type":"Person","data":{"name":"Zoe","age":33}}"#,
);
// Without --from, a missing branch must fail and create nothing.
let failure = output_failure(
cli()
.arg("load")
.arg("--mode")
.arg("merge")
.arg("--data")
.arg(&extra)
.arg("--branch")
.arg("feature-load")
.arg(graph.path()),
);
assert!(
String::from_utf8_lossy(&failure.stderr).contains("feature-load"),
"error should name the missing branch"
);
// With --from, the branch is forked and the load lands on it.
let payload = parse_stdout_json(&output_success(
cli()
.arg("load")
.arg("--mode")
.arg("merge")
.arg("--data")
.arg(&extra)
.arg("--branch")
.arg("feature-load")
.arg("--from")
.arg("main")
.arg(graph.path())
.arg("--json"),
));
assert_eq!(payload["branch"], "feature-load");
assert_eq!(payload["base_branch"], "main");
assert_eq!(payload["branch_created"], true);
assert_eq!(payload["mode"], "merge");
assert_eq!(payload["nodes_loaded"], 1);
let snapshot = parse_stdout_json(&output_success(
cli()
.arg("snapshot")
.arg(graph.path())
.arg("--branch")
.arg("feature-load")
.arg("--json"),
));
assert_eq!(snapshot["branch"], "feature-load");
}
/// `--mode` is required: overwrite is destructive, so the unified `load`
/// has no implicit default.
#[test]
fn local_cli_load_requires_mode_flag() {
let graph = SystemGraph::loaded();
let extra = graph.write_jsonl(
"system-local-load-no-mode.jsonl",
r#"{"type":"Person","data":{"name":"Zoe","age":33}}"#,
);
let failure = output_failure(
cli()
.arg("load")
.arg("--data")
.arg(&extra)
.arg(graph.path()),
);
assert!(
String::from_utf8_lossy(&failure.stderr).contains("--mode"),
"clap should demand the missing --mode flag"
);
}
#[test]
fn local_cli_export_round_trips_full_branch_graph() {
let graph = SystemGraph::loaded();
@ -512,6 +602,8 @@ fn local_cli_export_round_trips_full_branch_graph() {
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&export_path)
.arg(&imported_graph),
@ -610,6 +702,8 @@ policy: {{}}
cli()
.current_dir(query_root)
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(fixture("test.jsonl"))
.arg(&graph_uri),
@ -867,7 +961,15 @@ query get_task($slug: String) {
);
output_success(cli().arg("init").arg("--schema").arg(&schema).arg(&graph));
output_success(cli().arg("load").arg("--data").arg(&data).arg(&graph));
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(&graph),
);
let filtered = parse_stdout_json(&output_success(
cli()
@ -997,7 +1099,15 @@ query vector_search($q: String) {
);
output_success(cli().arg("init").arg("--schema").arg(&schema).arg(&graph));
output_success(cli().arg("load").arg("--data").arg(&data).arg(&graph));
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(&graph),
);
let result = parse_stdout_json(&output_success(
cli()
@ -1221,6 +1331,8 @@ fn local_cli_load_enforces_engine_layer_policy() {
.arg("--as")
.arg("act-bruno")
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--config")
.arg(&config)
.arg("--data")
@ -1239,6 +1351,8 @@ fn local_cli_load_enforces_engine_layer_policy() {
.arg("--as")
.arg("act-ragnor")
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--config")
.arg(&config)
.arg("--data")
@ -1684,6 +1798,8 @@ graphs:
std::fs::write(&data, "{\"type\":\"Person\",\"data\":{\"name\":\"Ada\"}}\n").unwrap();
let output = cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(temp.path().join("graphs/knowledge.omni"))
@ -1796,6 +1912,8 @@ fn seed_graph(dir: &std::path::Path, graph: &str, row: &str) {
std::fs::write(&data, row).unwrap();
let output = cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&data)
.arg(dir.join(format!("graphs/{graph}.omni")))

View file

@ -652,6 +652,8 @@ query add_friend($from: String, $to: String) {
output_success(
cli()
.arg("load")
.arg("--mode")
.arg("overwrite")
.arg("--data")
.arg(&export_path)
.arg(&imported_graph),
@ -755,6 +757,71 @@ fn remote_ingest_creates_review_branch_and_keeps_it_readable() {
assert_eq!(zoe["rows"][0]["p.name"], "Zoe");
}
/// The unified `load` works against remote graphs through the server's
/// `/ingest` endpoint: without `--from` a missing branch is a hard error
/// (no implicit fork), with `--from` it forks like ingest did.
#[test]
#[ignore = "requires loopback socket permissions in sandboxed runners"]
fn remote_load_round_trips_and_requires_from_for_new_branches() {
let graph = SystemGraph::loaded();
let server = graph.spawn_server();
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
let extra = graph.write_jsonl(
"system-remote-load.jsonl",
r#"{"type":"Person","data":{"name":"Zoe","age":33}}"#,
);
// Missing branch without --from: refused remotely, nothing created.
let failure = output_failure(
cli()
.arg("load")
.arg("--config")
.arg(&config)
.arg("--mode")
.arg("merge")
.arg("--data")
.arg(&extra)
.arg("--branch")
.arg("feature-load"),
);
assert!(
String::from_utf8_lossy(&failure.stderr).contains("feature-load"),
"error should name the missing branch"
);
// With --from, the remote load forks and lands the rows.
let payload = parse_stdout_json(&output_success(
cli()
.arg("load")
.arg("--config")
.arg(&config)
.arg("--mode")
.arg("merge")
.arg("--data")
.arg(&extra)
.arg("--branch")
.arg("feature-load")
.arg("--from")
.arg("main")
.arg("--json"),
));
assert_eq!(payload["branch"], "feature-load");
assert_eq!(payload["base_branch"], "main");
assert_eq!(payload["branch_created"], true);
assert_eq!(payload["nodes_loaded"], 1);
let snapshot = parse_stdout_json(&output_success(
cli()
.arg("snapshot")
.arg("--config")
.arg(&config)
.arg("--branch")
.arg("feature-load")
.arg("--json"),
));
assert_eq!(snapshot["branch"], "feature-load");
}
#[test]
#[ignore = "requires loopback socket permissions in sandboxed runners"]
fn remote_ingest_reuses_existing_branch_and_merges_updates() {