Skip to content

feat(cli): add cgc diff against saved index snapshots (#1312) - #1736

Draft
yashraj4 wants to merge 1 commit into
CodeGraphContext:mainfrom
yashraj4:feat/cgc-diff-snapshots
Draft

yashraj4 wants to merge 1 commit into
CodeGraphContext:mainfrom
yashraj4:feat/cgc-diff-snapshots

Conversation

@yashraj4

Copy link
Copy Markdown

Closes #1312.

Implements cgc snapshot save [--name], cgc snapshot list and cgc diff [--against] [--json] [--limit] [--fail-on-changes], so a graph state can be captured before a change and compared afterwards instead of re-querying by hand.

Format decision, JSON schema and exit codes were posted in the issue first, as requested: #1312 (comment)

Snapshot format

A snapshot is a lightweight JSON manifest, not a .cgc bundle, stored at ~/.codegraphcontext/snapshots/<name>.json (format: cgc-snapshot, format_version: 1, checked on read like bundle files):

  • cgc diff needs identity + content digest, not the payload, so manifests stay small enough to keep around (14-node probe repo → ~22 KB), and reading one never mutates the database the way loading a bundle would.
  • id is the graph's own primary key (CGCBundle._PK_MAP / _UID_PARTS, including uid synthesis for backends that do not persist it), so snapshots agree with cgc bundle export and across Neo4j/Kùzu.
  • digest strips volatile properties (Repository.indexed_at), so untouched code diffs to nothing.
  • Atomic writes, names restricted to ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$, existing names need --force.

JSON documents and exit codes

  • cgc diff --json → cgc-diff v1: base{}, current{}, summary{nodes, edges, total}, nodes{added, removed, changed}, edges{added, removed, changed}; changed rows carry reason (properties | moved | target), before/after and changed_properties[].
  • cgc snapshot save --json → cgc-snapshot-save receipt (summary, not the manifest); cgc snapshot list --json → cgc-snapshot-list (unreadable files become an error entry instead of hiding the rest).
  • Exit codes: 0 no differences / no gate requested, 1 differences under --fail-on-changes or a snapshot/initialisation error, 2 bad usage (invalid name, negative --limit).

Streams

Product on stdout — cgc diff --json > changes.json parses — init chatter and errors on stderr, following the #1725 split. Human output is ASCII only (+ - ~, ->, |), because a non-ASCII glyph raises UnicodeEncodeError on a cp1252 console the moment output is redirected.

Diff semantics

  1. A line shift is a move, not churn. A node uid embeds line_number, so typing a line above a function re-keys it; a naive set diff would report one deletion plus one identical addition. Unambiguous matches pair on a label + name + path + ownership chain signature and report as ~ … (moved, …); ambiguous pairs (two same-named functions in one file) stay add/remove rather than being guessed.
  2. Edge rows stay silent when only endpoint uids moved — an edge id embeds both endpoints, so otherwise every file edit triples the output.
  3. Retargets fold into one ~ CALLS a -> b (target changed) row only when exactly one removed and one added edge share source/type and the new target already existed in the base graph, so a file that gained refund while losing legacy_checkout does not claim its CONTAINS edge was retargeted.
  4. Owner-scoped names qualify (pay.amount), so same-named parameters stay distinguishable — via ownership edges only, so a CALLS edge never re-labels its callee.

Scope

CLI layer only: core/graph_snapshot.py (format/diff/render logic), cli/snapshot_commands.py (paths, orchestration, print() to stdout), Typer wiring in cli/main.py. No MCP tool, UI, database or bundle changes.

Tests

  • tests/unit/core/test_graph_snapshot.py — identity, pairing/ambiguity, retarget rules, documents, ASCII rendering, limit truncation.
  • tests/integration/test_snapshot_diff_cli.py — index → save → edit → re-index → diff, JSON parse, exit codes, stdout/stderr split (list-argv subprocesses, so it runs on Windows too).
  • tests/integration/cli/test_cli_commands.py — three new canonical-matrix entries, with the fake session answering the snapshot queries, so the inventory meta-test covers the new commands.

ruff clean on all new/changed files; tests/unit matches the pre-existing baseline (8 known failures, verified against a864a9f).

…xt#1312)

Closes CodeGraphContext#1312.

Adds `cgc snapshot save [--name]`, `cgc snapshot list` and `cgc diff [--against]`, so a graph state can be captured before a change and compared afterwards instead of re-querying by hand.

A snapshot is a lightweight JSON manifest (`~/.codegraphcontext/snapshots/<name>.json`, `format: cgc-snapshot`, `format_version: 1`), not a `.cgc` bundle: a diff needs identities and content digests, not the payload, so the manifest stays small enough to keep around. Identities come from `CGCBundle._PK_MAP`/`_UID_PARTS` — the same primary keys bundle export uses, so both documents agree on what a node is — and digests strip volatile properties (`Repository.indexed_at`), so two snapshots of untouched code compare equal.

Because a node `uid` embeds `line_number`, typing a line above a function re-keys it; a naive set diff would report that as one deletion plus one identical addition and bury everything real under churn. `graph_snapshot` therefore pairs unambiguous matches by a name/path/ownership signature (a shifted node reports as `moved`, not as churn), leaves genuinely ambiguous pairs alone rather than guessing, folds an edge retarget into one `target changed` row only when the new target already existed in the base graph, and qualifies owner-scoped names (`pay.amount`) so same-named parameters stay distinguishable.

The product goes to stdout — `cgc diff --json > changes.json` parses — with init chatter and errors on stderr, following the CodeGraphContext#1725 split. Exit codes: 0 no differences (or no gate requested), 1 differences under `--fail-on-changes` and snapshot/initialisation errors, 2 bad usage. Human output is ASCII so redirected output survives a non-UTF-8 console.

Scoped to the CLI layer: `core/graph_snapshot.py`, `cli/snapshot_commands.py` and the Typer wiring in `cli/main.py`. Covered by `tests/unit/core/test_graph_snapshot.py`, `tests/integration/test_snapshot_diff_cli.py`, and three new entries in the canonical command matrix.
@vercel

vercel Bot commented Sep 24, 2026

Copy link
Copy Markdown

@yashraj4 is attempting to deploy a commit to the shashankss1205's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Shashankss1205 Shashankss1205 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substantial snapshot-diff CLI feature. Local octopus integration with other drafts fails in parity verification code despite stale mergeability metadata. Rebase individually, validate snapshot path/graph semantics, then review when marked ready.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog tasks

Development

Successfully merging this pull request may close these issues.

feat: add cgc diff — CLI command to show graph changes between index snapshots

2 participants