Establish single total order for commit lineage; list newest-first#383
Conversation
…ge_key The lineage total order previously existed as two hand-duplicated comparators (the load_commits sort and should_replace_head) plus a positional .rfind in recovery that silently encoded "ascending". Introduce GraphCommit::lineage_key as the one definition, derive both comparators from it, and replace recovery's positional scan with an intent-named CommitGraph::latest_commit_matching so no caller couples to iteration direction. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
…nch parity row The public listing assertions fail against the current ascending order with the predicted symptom (first element is not the branch head); the next commit turns them green. The parity row covers the previously untested `commit list --branch` remote query-param fork and is green on both sides of the flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
Omnigraph::list_commits — the one door the embedded CLI, HTTP server, and SDK all funnel through — now returns the branch's reachable history most recent first, matching the ordering the OpenAPI description has promised all along (the implementation shipped ascending). The internal projection stays ascending; recovery, merge-base, and head selection are untouched (they no longer depend on iteration direction after the lineage_key refactor). Also corrects the second stale claim in the /commits description: omitting `branch` lists main's history, not "across all branches" — stale since RFC-013 Phase 7 retired the global _graph_commits.lance table. Ordering is documented as part of the contract, with future cursor/limit pagination reserved as keyset-based on the same order. This is a change to shipped observable behavior (Hyrum's-law item in the deny-list): consumers indexing the listing positionally must use [0]/first() for the head where they previously used last(). Record in the next release's notes at the release boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4955517ec2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// fork plus the branch-authored commits); omitting it returns `main`'s | ||
| /// history. There is no cross-branch listing. Ordering is part of the |
There was a problem hiding this comment.
Authorize omitted commit branch as main
When a Cedar policy grants read only for protected/main branches, GET /commits without branch is now documented and implemented as main history, but the handler still builds PolicyRequest { branch: query.branch.clone(), ... }, leaving branch as None. That makes protected/unprotected branch-scope rules see context.has_branch == false and incorrectly deny the default-main request; normalize the omitted query to Some("main") before authorizing, as the snapshot/export handlers do.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed valid and fixed in 55d0052 (red test) + 7eecb2a (fix). The handler now normalizes the omitted branch to main before building the PolicyRequest, exactly as server_snapshot/export do, so a read grant with branch_scope: protected treats plain GET /commits the same as GET /commits?branch=main.
Verification: the new auth_policy.rs::policy_authorizes_omitted_commit_list_branch_as_main test reproduces the 403-vs-200 split at the test commit and asserts the omitted and explicit forms return identical bodies after the fix. Within the supported rule surface (any/protected/unprotected, all requiring has_branch when scoped), the change only ever makes the omitted form match the explicit ?branch=main outcome. The remaining branch: None reads (schema get, branch list, commit show by id) are genuinely graph-scoped and unchanged.
Generated by Claude Code
GET /commits without ?branch is documented and implemented as main's history, so under a read grant scoped to protected branches (main protected) it must behave exactly like the explicit ?branch=main form. Today the handler authorizes with the raw Option (branch: None), so has_branch=false and the protected-scoped permit never matches: the explicit form returns 200 while the omitted form returns 403. Red at this commit with that exact split; the next commit normalizes the omitted branch before authorizing, as server_snapshot already does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
Normalize the omitted `branch` query to `main` before building the PolicyRequest, mirroring server_snapshot and export. The handler executes an omitted branch as main's history, but authorized it with branch: None — and branch-scoped Cedar grants compile to `context.has_branch && ...`, so under default-deny a read grant scoped to protected branches permitted GET /commits?branch=main while denying the equivalent plain GET /commits. Within the supported rule surface (any/protected/unprotected) this only ever makes the omitted form behave exactly like the explicit ?branch=main form. The graph-scoped reads (schema get, branch list, commit show by id) correctly keep branch: None and are unchanged. Turns the previous commit's red test green. Raised by PR #383 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
What & why
This change establishes a single canonical total order for commit lineage —
(manifest_version, created_at, graph_commit_id)— and makes it the foundation for all ordered views of commits. The publiclist_commitsAPI now returns commits newest-first (reversed from the internal ascending order), making the most recent commit the first element. This aligns the public contract with user expectations and prepares the foundation for future keyset-based pagination.Key changes:
Extract lineage ordering to
GraphCommit::lineage_key(): A new method returns the canonical(u64, i64, &str)tuple that defines commit order. All comparisons now derive from this single source of truth.Simplify internal sorting:
CommitGraph::load_commits()now useslineage_key()instead of inline multi-field comparisons. The internal projection remains ascending (oldest-first).Add
latest_commit_matching(): A new internal helper finds the maximal commit satisfying a predicate, usinglineage_key()for comparison. This replaces imperative iteration patterns (e.g., in recovery code).Reverse public listing:
Omnigraph::list_commits()now reverses the internal ascending order before returning, presenting newest-first to all consumers (CLI, HTTP server, SDK).Update head selection logic:
should_replace_head()now useslineage_key()comparison, ensuring head selection follows the same total order.Update documentation and tests: API docs clarify the newest-first contract and keyset-pagination intent. Tests verify the ordering invariant and branch-specific listing behavior.
Backing issue / RFC
Checklist
lineage_projection.rsverifies public listing is strictly descending bylineage_keymaintenance.rsandauth_policy.rsto use.first()instead of.last()(newest-first)commit list --branchin CLIomnigraph-serverhandler docs to clarify newest-first ordering and keyset-pagination intent--branchsemanticsNotes for reviewers
Omnigraph::list_commits()reverses; internal code (CommitGraph::load_commits(), recovery, head selection) remains ascending. This keeps internal logic simple while meeting the public contract.(manifest_version, created_at, graph_commit_id)is the single source of truth. Future pagination cursors will be keyset-based on this same order.lineage_projection.rspins the public ordering contract; existing tests updated to reflect newest-first indexing.converge_or_defer_roll_forward()now useslatest_commit_matching()instead ofrfind(), making intent clearer.https://claude.ai/code/session_01WXeTJLbMK5dHcQarn6UGFL
Note
Medium Risk
Changes the public commit-list ordering contract and HTTP policy normalization for omitted branch; impact is broad but localized to one API boundary with updated tests and docs.
Overview
Introduces
GraphCommit::lineage_key()as the single total order(manifest_version, created_at, commit_id)for sorting, head selection, and future keyset pagination. InternalCommitGraphviews stay oldest-first;Omnigraph::list_commitsis the only public flip to newest-first for CLI, HTTP, and SDK.GET /commitsnow treats an omittedbranchasmainfor Cedar (matching snapshot behavior) and documents branch-scoped history only—no cross-branch listing. OpenAPI, user docs, and CLI reference reflect the contract.Recovery uses
latest_commit_matchinginstead of positional scans; tests and parity coverage move from.last()to.first()for head commits and addcommit list --branchparity.Reviewed by Cursor Bugbot for commit 7eecb2a. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
Establishes a canonical commit-lineage order and exposes public commit listings newest-first.
GraphCommit::lineage_key()for sorting, head selection, and recovery lookup.Omnigraph::list_commits()while retaining ascending internal projections.mainfor authorization and lookup.Confidence Score: 5/5
The PR appears safe to merge with no blocking failures remaining.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR M[Manifest lineage rows] --> C[CommitGraph] C -->|ascending lineage_key| I[Internal projection] I -->|reverse| O[Omnigraph list_commits] O -->|newest first| H[HTTP API] O -->|newest first| E[Embedded CLI and SDK]Reviews (2): Last reviewed commit: "fix(server): authorize omitted commit-li..." | Re-trigger Greptile