Skip to content

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

Open
RiceTooCold wants to merge 6 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 6 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 4 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
…, shallow flag

The three operational bounds for the history walk:

- cbm_proc_*: deadline-bounded command reader in the foundation layer
  (POSIX pipe+fork of sh -c in its own process group, poll-driven reads,
  kill on expiry so close never blocks; Windows falls back to _popen
  without a deadline — documented follow-up). The four get_node_history
  git call sites move from cbm_popen to it: 30s for the -L walk, 10s
  for rev-parse / diff / show. A timed-out walk is refused, not served
  partially, with advice to bound the query.

- since/until: charset-validated git dates passed through to the walk.
  Windowed queries bypass the cache both ways — partial timelines are
  neither served from nor persisted to the full-timeline cache.

- shallow: detected in the same rev-parse invocation as HEAD; the
  response says the timeline is truncated by clone depth, not history.

Tests: git-date validation, proc reader (lines/exit status, fgets-style
splits, deadline kill). Security allowlist updated for the new call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c3No8anx3TLVa2n5uBAnC
…ive paths

git show reports paths relative to the git root, but the node's file
path is relative to the indexed root, which may be a subdirectory
("src/" of a monorepo) — the exact-match self-filter never fired there
and the symbol's own file topped its co-change list. Suffix match with
a '/' boundary. Found dogfooding against a Next.js repo indexed at src/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c3No8anx3TLVa2n5uBAnC
RiceTooCold pushed a commit that referenced this pull request Jul 12, 2026
… discovery/FQN

Comprehensively reproduce the remaining QUALITY_ANALYSIS gaps:

- repro_invariant_enclosing_parity (gap DeusData#3): cbm_find_enclosing_func's hardcoded
  func_kinds_for_lang switch has drifted from lang_specs.function_node_types.
  Languages absent from the switch whose function nodes aren't in func_kinds_generic
  silently attribute every in-function call to Module. Full drift table +
  per-language reproductions (fortran/scss/sql/verilog/julia/nix RED via Module
  source; commonlisp/emacslisp/dart/cobol RED, some compounded by a callee-
  extraction gap).
- repro_invariant_lsp_rescue (gap DeusData#5/#5a): cbm_pipeline_find_lsp_resolution
  (lsp_resolve.h:65) joins LSP results to tree-sitter calls by EXACT
  caller_qn==enclosing_func_qn; when tree-sitter says Module, the LSP rescue is
  discarded. C++ out-of-line fixture asserts the edge is LSP-callable-sourced AND
  that properties_json preserves the lsp_* strategy/confidence (both RED today).
- repro_invariant_discovery_fqn (gaps #1,DeusData#4): comprehensive 50+ skip-dir table
  (.claude-worktrees RED, the rest GREEN guards) + 6 FQN same-stem collision cases
  (api.h/api.c + svc.h/svc.cpp RED; cross-dir/.d.ts/cross-package GREEN guards).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
RiceTooCold pushed a commit that referenced this pull request Jul 12, 2026
QUALITY_ANALYSIS gap #1: .claude-worktrees was indexed (nearly tripling this
repo's indexed surface + polluting queries with stale duplicate symbols).
Discovery skipped .claude and .worktrees but not .claude-worktrees. Add it to
ALWAYS_SKIP_DIRS. Flips the discovery-hygiene invariant green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
RiceTooCold pushed a commit that referenced this pull request Jul 12, 2026
…een guard

QUALITY_ANALYSIS gap #1 (.claude-worktrees absent from ALWAYS_SKIP_DIRS) is
fixed — src/discover/discover.c now lists ".claude-worktrees" next to ".claude",
so cbm_discover() correctly skips it. The reproduction's RED expectation
(expected the canary to be indexed) was stale and the lifecycle assertion fired.
Reclassify it as a GREEN guard (expect skipped) so it now protects against
regressing the skip, per the test's own RED-becomes-GREEN workflow.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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