Skip to content

feat(mcp): get_node_history — per-symbol git timeline, lazy + sparse#1

Open
RiceTooCold wants to merge 4 commits into
mainfrom
claude/git-history-timeline-464cbx
Open

feat(mcp): get_node_history — per-symbol git timeline, lazy + sparse#1
RiceTooCold wants to merge 4 commits into
mainfrom
claude/git-history-timeline-464cbx

Conversation

@RiceTooCold

Copy link
Copy Markdown
Owner

Per-symbol git history timeline (get_node_history)

Extends the temporal layer from upstream DeusData#257 (file-level change_count/last_modified scalars) down to symbol granularity: a new MCP/CLI tool that returns the evolution timeline of any line-scoped node (Function/Method/Class) — every commit that touched its line range, newest first, with author/date/subject and per-commit line stats.

Motivation

Agents can answer what/where with the graph, but not why/when:

  • "Why is this magic number here — can I remove it?" (Chesterton's fence)
  • "Has this function been reverted before? When did it last change?"
  • "What historically changes in the same commits as this symbol?" (function↔test pairs, serializer/deserializer coupling — invisible to static edges)

This matters doubly for agent environments: CI and web agents usually run on shallow clones, where git log -L / --follow / pickaxe return nothing. The timeline is cached in the graph DB keyed by HEAD, so a carried DB/artifact answers with a full timeline in exactly the environments where git archaeology is impossible (verified: --depth 1 clone → git sees 1 commit, tool serves the full 21-revision history as a cache hit).

Design (sparse versioning, on-demand queryability)

  • Lazy + cached: first query runs git log -L<start>,<end>:<file> (~58 ms cold), persists metadata into two new tables (node_revisions, node_revision_meta) keyed by repo HEAD; warm queries (~24 ms) never touch git. No libgit2 — aligns with upstream chore: drop optional libgit2 dependency (keep git log fallback) DeusData/codebase-memory-mcp#865.
  • Sparse storage: patch text is never stored (git is already a content-addressed store of every version); include_patch: true regenerates diffs on demand. Full warm of all 8,725 symbols in this repo: 61 s, 16,594 revisions, 5.4 MiB.
  • Lazy schema: tables are created on first use, so query-path opens (cbm_store_open_path_query) and pre-existing DBs upgrade in place; projects that never use the feature never grow the tables.
  • Working-tree drift mapping: indexed line numbers reflect the working tree but git log -L anchors at HEAD — uncommitted edits above a symbol would silently shift its history onto the wrong lines. The handler maps the range back to HEAD coordinates via git diff -U0 HEAD hunk headers; dirty files bypass the cache (cache: "computed_dirty", mapped range reported).
  • co_changed: true: files that most often changed in the same commits as this symbol — temporal coupling at symbol granularity, generalizing FILE_CHANGES_WITH.

Changes

  • src/store/store.{c,h}: cbm_node_revision_t + transactional replace/get/head CRUD (prepared-statement style), lazy schema guard
  • src/mcp/mcp.{c,h}: get_node_history handler (resolution tiers identical to get_code_snippet, quoting/validation per detect_changes), exported pure cbm_range_map_* functions for the drift mapper
  • tests/test_node_history.c: 15 tests — store CRUD roundtrip/overwrite/isolation incl. lazy schema on a memory store; range mapper: insert/delete above, overlap, below-range, multi-hunk accumulation, count-omitted single-line hunks, clamping (all passing)
  • tests/test_main.c, Makefile.cbm: suite registration
  • README.md, CONTRIBUTING.md, mcp.{c,h} headers: tool count 14 → 15, feature/tool-table docs

No changes to existing schema paths, passes, or tools. Windows command variants follow the existing detect_changes pattern (not yet exercised on a Windows runner).

Verification

  • CLI end-to-end on this repo (12k nodes / 800 commits): cold 58 ms → warm 24 ms cache hit; include_patch regenerates exact diffs; co_changed surfaces real coupling (e.g. handle_detect_changesstore.c in 7 of 11 revisions)
  • Drift fix verified on a live dirty tree: node shifted +200 lines by uncommitted edits still resolves its full 7-revision history (was 1 wrong revision before the mapper)
  • Full test suite: the 15 new tests pass; the only red is the pre-existing extraction_imports LSan leak on main (byte-identical to baseline, unrelated paths)

Follow-ups (separate PRs): opt-in eager warm pass in BEST mode, find_temporal_coupling/risk_hotspots query tools over the warmed table, commit→symbols reverse index for regression localization, rename/extract lineage stitching.

🤖 Generated with Claude Code

https://claude.ai/code/session_019w17HvR6LS8wk1DPDDdUHH


Generated by Claude Code

claude added 2 commits July 6, 2026 07:57
New MCP/CLI tool: the evolution timeline of any line-scoped graph node
(function/class/method) — every commit that touched its line range,
newest first, with author/date/subject and per-commit line stats.

Design: sparse versioning with on-demand query power.
- Metadata only in SQLite (node_revisions + node_revision_meta, created
  lazily on first use so query-path opens and pre-existing DBs upgrade
  in place). Patch text is never stored: git is already a
  content-addressed store of every version, so diffs are regenerated
  on demand via include_patch.
- Lazy compute + cache: first query runs git log -L (~50ms) and caches
  until HEAD moves; warm queries serve from SQLite (~25ms).
- Working-tree drift mapping: indexed line numbers are remapped to HEAD
  coordinates by parsing git diff -U0 hunk headers, so uncommitted
  edits above a node no longer shift its history onto the wrong lines.
  Dirty files bypass the cache and are flagged computed_dirty.
- co_changed option surfaces temporal coupling: files that repeatedly
  changed in the same commits as the symbol, which static edges miss.

Benchmarked on this repo (6 provenance/coupling/volatility questions,
A/B vs a raw-git baseline agent): answer parity after the drift fix,
with single get_node_history calls replacing git-log + awk pipelines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019w17HvR6LS8wk1DPDDdUHH
…15th tool

Upstream-readiness pass for get_node_history:

- Extract the working-tree→HEAD line-range mapper into exported pure
  functions (cbm_range_map_init/hunk/finish) so the drift math is unit-
  testable without git, mirroring the pass_gitdiff parser precedent.
- Add tests/test_node_history.c (15 tests): node_revisions CRUD
  roundtrip/overwrite/isolation on a memory store incl. lazy schema, and
  range-mapper cases (insert/delete above, overlap, below-range, multi-
  hunk accumulation, count-omitted single-line hunks, clamping).
- Register the suite in test_main.c and Makefile.cbm TEST_STORE_SRCS.
- Update tool counts (14 -> 15) in mcp.h/mcp.c/README/CONTRIBUTING and
  document get_node_history in the README feature list and tool table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019w17HvR6LS8wk1DPDDdUHH
Copilot AI review requested due to automatic review settings July 6, 2026 09:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new MCP/CLI capability (get_node_history) that provides per-symbol git history (timeline of commits touching a symbol’s line range), with lazy computation and caching in the project SQLite store to work even in shallow-clone agent environments.

Changes:

  • Added a node-revision cache to the SQLite store (lazy schema + CRUD APIs) for per-symbol history metadata.
  • Implemented the get_node_history MCP tool, including a working-tree→HEAD line-range drift mapper and optional patch/co-change output.
  • Added a dedicated test suite for node revision storage and range mapping; updated docs/tool counts and wired tests into the build.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_node_history.c New unit tests for node revision store CRUD and range mapping logic.
tests/test_main.c Registers the new node_history test suite.
src/store/store.h Adds public types/APIs for cached node revision timelines.
src/store/store.c Implements lazy schema creation and node revision cache read/write paths.
src/mcp/mcp.h Exposes range-mapping API for unit tests and updates tool count comment.
src/mcp/mcp.c Adds get_node_history tool implementation (git parsing, caching, coupling, drift mapping).
README.md Documents the new tool and updates tool-count references.
Makefile.cbm Includes the new test source in the test build.
CONTRIBUTING.md Updates repo structure docs for the new tool count.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/store/store.c Outdated
Comment on lines +1787 to +1790
sqlite3_stmt *stmt =
prepare_cached(s, &s->stmt_get_node_revs,
"SELECT sha, ts, author, subject, added, deleted FROM node_revisions "
"WHERE project = ?1 AND qualified_name = ?2 ORDER BY ts DESC;");
Comment thread src/mcp/mcp.c Outdated
Comment on lines +4744 to +4749
if (include_patch && fresh && i < fresh_count && i < patch_limit && fresh[i].patch) {
yyjson_mut_obj_add_strcpy(doc, item, "patch", fresh[i].patch);
if (fresh[i].patch_truncated) {
yyjson_mut_obj_add_bool(doc, item, "patch_truncated", true);
}
}
Comment thread src/mcp/mcp.c
Comment on lines +4564 to +4567
int limit = cbm_mcp_get_int_arg(args, "limit", NH_DEFAULT_LIMIT);
bool include_patch = cbm_mcp_get_bool_arg(args, "include_patch");
int patch_limit = cbm_mcp_get_int_arg(args, "patch_limit", NH_DEFAULT_PATCH_LIMIT);
bool co_changed = cbm_mcp_get_bool_arg(args, "co_changed");
claude and others added 2 commits July 7, 2026 00:20
The audit is file:function pair-based so src/mcp/mcp.c:cbm_popen already
passes, but CONTRIBUTING requires each new popen call site to carry an
explicit justification entry for human review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019w17HvR6LS8wk1DPDDdUHH
…idity, staleness guard

Review findings from an 8-angle adversarial pass, all verified against
live repros before fixing:

- Patch misattribution: emit computed results straight from git's output
  (miss path) and sha-match patches on cache hits — the write-then-read-
  back roundtrip paired `git log` order with `ORDER BY ts DESC` by index,
  attaching diffs to the wrong commit after rebases/cherry-picks.
- Silent-empty timelines: persist is now best-effort on the miss path
  (response no longer depends on the store write), and a failed cache
  read on a hit returns an explicit error instead of total_revisions: 0.
- Staleness guard: compare the file's mtime+size against its file_hashes
  row (new cbm_store_get_file_hash) before trusting indexed line numbers;
  stale files get an actionable re-index error instead of a double-shifted
  range whose wrong history was then cached under HEAD.
- Cache poisoning: overlap-approximated (dirty) computations are no longer
  persisted, so a later clean tree at the same HEAD can't serve them as
  authoritative hits.
- Uncommitted symbols: a pure-insertion hunk swallowing the whole range
  now short-circuits to total_revisions: 0 with an explanatory note —
  previously git log died past HEAD's EOF and the error blamed a stale
  index that reindexing could never fix.
- include_patch no longer defeats the cache: warm hits re-run git with
  --max-count=patch_limit (all calls now bounded by NH_MAX_REVS) and skip
  the redundant cache rewrite; edits entirely below a symbol (zero shift,
  no overlap) no longer void its cache.
- added/deleted counters: hunk-boundary state machine instead of a bare
  +++/--- prefix test, which miscounted column-0 content like "--i;" or
  SQL "-- comment" lines (reproduced against real git output).
- Schema: node_revisions/node_revision_meta gain the same
  REFERENCES projects(name) ON DELETE CASCADE as every other per-project
  table (delete_project no longer orphans timeline rows); dropped the
  unqueried idx_noderev_sha; deterministic ORDER BY tiebreaker.
- nh_append_patch: O(n) via tracked length; co_changed command buffer
  offset clamped; three duplicate stat_mtime_ns copies consolidated into
  platform.h cbm_stat_mtime_ns.
- Docs/tests: installer template, README, npm README now list all 15
  tools incl. get_node_history; tools/list and skill-content assertions
  updated; new mapper tests for the uncommitted-detection cases.

Full suite: 4971 passed; the 88 reds are main's intentional probe
reproduction suites, byte-identical to baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c3No8anx3TLVa2n5uBAnC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants