Repository navigation
perf(changes): resolve diff sides once, skip commit lookups for branch names, use full CLI pages - #968
Merged
ragnorc merged 19 commits intoOct 14, 2026
Conversation
…ed contract Amend RFC 0030 with a public graph diff over two read targets, a branch review form against the merge base, `branch merge-base`, and a read-only `branch merge --dry-run`. Keep `commit changes` as the cause-carrying unit, retire the table-shaped `ChangeSet`, and reshape C6 around the per-commit schema content storage format 14 retains. Adds D0 evidence gates, D1-D3 phases and a decision log recording how each premise was checked.
Implement RFC 0030 D1-D3 on the engine, HTTP and the CLI. - Omnigraph::diff_page reports the net entity changes between two branches or commits in the commit-changes shape, with a diff-page token that keeps the first page's resolved endpoints. A side naming both a branch and a commit is AmbiguousReadTarget; one naming neither is ReadTargetNotFound. - Omnigraph::merge_base returns the commit a merge would classify against, by the merge's own rule. - Session::branch_merge_dry_run runs a merge's capture, classification and validation and stops before any effect. - GET /diff, GET /branches/diff, GET /branches/merge-base and dry_run on POST /branches/merge; CLI diff, branch diff, branch merge-base and branch merge --dry-run. A /diff side the actor cannot read answers the same 404 as an unknown side. - The table-shaped diff_between/diff_commits and ChangeSet are removed; the DST oracle and tests move to diff_page. - GQT gains --- diff, --- merge-base and --- merge-dry-run steps (RFC 0045) with four cases on the engine, DST and served targets.
…a branch diff Address the review of the graph diff amendment. - A merge base is a valid feed start only on the source's first-parent chain. After the target was merged into the source, the base is the imported target commit and the feed refuses it; point at branch diff for the net change and add the shape to the D0 gates. - The branch diff and merge base require read on both named branches, continuations included: their endpoints, the base and the source head, can both be readable while the target is not. Add the D0 gate. - Defer GQ exposure of the diff surfaces to RFC 0056 rather than reject it.
…raph-diff-impl # Conflicts: # docs/rfcs/0030-cdc-time-travel.md
…ed branch review Follow the RFC 0030 review corrections: - The user guide's merge-base recipe now holds only while the source has never merged the target in; after that the base is off the source's first-parent chain, the feed refuses it, and branch diff gives the net change. The existing merge-and-feed test asserts that shape. - The branch diff and merge base already authorize read on source and target. A new policy test pins the case where both diff endpoints are readable but the target is not, on the first page, a continuation and the merge base.
…h names, use full CLI pages - A diff side whose spelling is neither a ULID nor an hb1 address cannot be a commit id, so classification no longer looks it up as one. The lookup's miss path checked out every live branch's __manifest, so a diff between two branch names cost about 22 requests per live branch. A cost gate pins the reads flat in idle branches. - Omnigraph::prepare_diff_page resolves a request once and reports the branch each two-point side reads; the server authorizes those and runs the page with run_diff_page, so authorization and the read share one resolution. diff_read_target_branch is removed. - When the CLI follows every page of commit changes, changes poll, diff or branch diff, it asks for 8,192 changes per page unless --limit is set: every page restarts its comparison, so fewer pages cost less.
This was referenced Oct 13, 2026
The DST history audit now checks each adjacent pair of recorded commits through diff_page instead of the retired diff_commits. diff_page resolves each side as a read target, which also checks the branch namespace so an ambiguous ULID is refused, and the audit pays 4 more gets in total (_audit l.get 724 -> 728). No engine operation's count changes.
A base off the source's first-parent chain is refused as a feed start. A merge of the target into a diverged source is one way to get there, and a later merge back can bring the base onto the chain again, so the RFC no longer says any past target-to-source merge prevents the recipe.
…raph-diff-impl # Conflicts: # docs/rfcs/0030-cdc-time-travel.md
… statements
Review asked that the diff surfaces be GQ statements under branch_stmt,
beside `branch merge ... as commit`, rather than GQT step kinds.
- Grammar and AST: `branch diff <source> [into <target>]`, `branch diff
<from> to <to>` with `after page "<token>"` and `limit <n>`, `branch
merge-base <source> [into <target>]`, and `branch merge <source> [into
<target>] dry run`, a BranchStmt::Read control read. A bare `dry run`
is the clause, as `as commit` is.
- Server: the statements enter POST /query and are refused at POST
/mutate as reads. Each runs its route's body (shared helpers now behind
GET /diff, /branches/diff, /branches/merge-base and the merge dry run),
so authorization, the 403/404 decisions and the dry run's 409 match.
- Engine and API types: Session::branch_read answers a statement for
embedded callers; branch_read_output renders it. A branch diff answers
one row per change, the type named by its name, with the compared
commits and the continuation token in a new optional `page` object on
ReadOutput, the shape RFC 0056 proposes.
- CLI: `omnigraph query -e` runs the statements locally and remotely; the
table, kv and jsonl renderings show the page state.
- GQT: the three step kinds are withdrawn. Branch reads are `--- query`
rows steps without a shape section, taking a bound `${name}` in a quoted
side and in expected rows; the four graph_diff cases use them on the
engine, DST and served targets.
- RFC 0030 §8.5, RFC 0045, RFC 0055, user docs, the changelog note and
openapi.json follow.
…costs # Conflicts: # crates/omnigraph-server/src/handlers.rs
…uest-costs # Conflicts: # crates/omnigraph-cli/src/cli.rs # crates/omnigraph-cli/src/main.rs # crates/omnigraph-server/src/handlers.rs # crates/omnigraph/src/db/omnigraph.rs # crates/omnigraph/tests/forbidden_apis.rs # crates/omnigraph/tests/integration/changes.rs
5 tasks done
Review of
|
| Command | Result |
|---|---|
engine --test integration changes:: changes_cost:: |
64 passed, 0 failed, 1 ignored (an existing repair ignore) |
changes_cost::graph_diff_side_resolution_is_flat_in_live_branches --nocapture |
manifest_reads 5 → 5 (1 → 9 idle branches) |
| same test, with the spelling gate disabled locally (reverted) | fails: 15 → 31 |
server --test integration auth_policy:: data_routes:: openapi:: |
224 passed, 0 failed |
temporary /diff probe test (not pushed), run at head and at merge base |
results in the inline comments |
cargo fmt --all --check |
pass |
check-docs.py (pinned venv), typos 1.50.1, check-agents-md.sh |
pass (269 Markdown files) |
CI on 81ac9d1a |
all checks green, including Test Workspace, Lint (clippy), GQ Logic Tests and CI Gate |
Limitations: I did not run CLI parity_matrix:: / cli_data::, served GQT, forbidden_apis or workspace clippy locally; I relied on CI for those. I did not run the S3 or Azure suites. I did not measure the 19.5 s → 7.3 s CLI timing.
`/diff` resolved both sides before it authorized either, so with a forbidden `from` an unknown `to` answered first and the error named whichever side the policy hid. The engine now takes the caller's read check (`diff_page_readable`) and authorizes each two-point side as soon as it is classified, before the next side is looked at or a snapshot opens; a refused side is the not-found of a side that names nothing. A continuation authorizes its sides again before it reopens the token's commits. The two-phase `prepare_diff_page` / `run_diff_page` API is gone, so no transport has to get the order right. A diff endpoint commit whose writing branch is gone is the not-found of the spelling that named it, mapped where the engine knows that spelling, instead of the transport guessing a side from a branch name. One helper decides a side's spelling. The CLI page-size note and the changes guide say that every page restarts its comparison, and the note names `diff` and `branch diff`.
…uest-costs # Conflicts: # crates/omnigraph/src/db/omnigraph.rs
This was referenced Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #966 (graph diff, branch review and merge dry run). Review that first; this PR's own change is the last commit.
This PR takes the three cheap fixes from a measured investigation of graph diff cost. The deeper bottlenecks it found are filed as issues (linked below).
Changes
1. A diff side that cannot be a commit id is never looked up as one. Every graph commit id is minted canonical, either a ULID or an
hb1address (is_valid_graph_commit_id). Classification now skips the commit lookup for any other spelling. Before this, a side likemaintook the lookup's miss path, which checks out and scans every live branch's__manifest.Measured under DST: a one-change diff between two branch names cost about 22 requests per live branch. With 32 idle branches that was 795 requests, of which 23 read the table (about 7 s under the S3-like model). The new cost gate shows the same diff reading 5
__manifestobjects with 1 idle branch and with 9. Without the gate it read 15 and 31.A malformed
hb1.spelling is now a typedReadTargetNotFoundinstead of an internal error from the id parser.2. One resolution serves authorization and the read.
/diffused to classify each side once to authorize it (diff_read_target_branch), and the engine classified it again to read. That doubled the cost, and the two resolutions could disagree if a branch was created or deleted in between.Omnigraph::prepare_diff_pagenow resolves the request once and reports the branch each two-point side reads (PreparedDiffPage::side_branches). The server authorizes those branches, thenrun_diff_pagereads.diff_pageis the one-call form for embedded callers.diff_read_target_branchis removed. A continuation still classifies each spelled side for authorization and reads the commits its token pins, as before.3. The CLI asks for full pages when it follows every page. Every change page restarts its comparison: it re-plans, re-resolves its endpoints and re-sorts the remaining keys. Measured on a release build, a 100k-change diff over 1M rows took 19.5 s at the 1,000-change default and 7.3 s at 8,192.
commit changes,changes poll,diffandbranch diffnow ask for 8,192 changes per page when they paginate themselves, unless--limitis given. A single--page-tokenfetch keeps the 1,000 default, and so do the HTTP routes. The--limithelp text no longer claims the server clamps it: an over-ceiling limit is refused.Tests
changes_cost::graph_diff_side_resolution_is_flat_in_live_branches: a branch-name diff with 1 and 9 idle branches,__manifestreads gated flat. It fails with the spelling gate removed (15 → 31 reads) and passes with it (5 → 5).changes::graph_diff_refuses_an_ambiguous_or_unknown_sidenow also covers:hb1.side, which is a typed not-found;prepare_diff_pagereporting("main", "main")and(<commit>, "feature")as the side branches.RUST_MIN_STACK=16777216, all passing:changes::andchanges_cost::(65), andforbidden_apis(35; the new public methods are registered as read-only);auth_policy::,data_routes::andopenapi::(224): the 404 parity for forbidden sides is unchanged;parity_matrix::andcli_data::(119);graph_diff_*cases;cargo fmt --check,check-docs.pyandtypos.Not in this PR (filed as issues)
__manifest(about 4 requests per branch for commit-id sides).