memory.code_context¶
memory.code_context ¶
Code-graph query and code ↔ memory tether helpers for the memory MCP server.
This module is the analysis substrate behind :func:memory.server.codebase's
action dispatcher. It mirrors :mod:memory.retrieval: pure functions that take
the live objects they operate on (the kglite code graph, a dulwich repo,
the repo root, and — for tethers — a pre-collected list of memory entries)
and return plain dicts, so the dispatcher in server.py stays thin and every
function here is importable and mockable from tests.
It holds three things the design (documents/260720_code-memory-tethers-design.md)
calls for:
- Structure queries —
blast(reverse dependency BFS),callers/callees(1-hopCALLSedges carryingcall_lines), and theimportsneighborhood, all with built-asset hygiene and depth/node caps. - Code ↔ memory tethers — the "touched" invert (per entry pin
path@sha, diff the pin commit against its FIRST PARENT for that path, map changed line ranges onto kglite symbol ranges), the rollingtip, and the append-only provenancechain. Anchor identity is aqualified_nameresolved against the live graph at query time; unsupported languages degrade to FILE anchors. - Health — a graded, located driver list (god object, coupling, cycle, churn, dead code) built from degree-style centrality over the code graph.
Every payload is decision-shaped: ranked, capped, with file:line and a
next hint naming the follow-on call.
Tether soundness (the crux). A pin's first-parent diff is only trusted as the
entry's edit (changed) for a single-parent commit that actually touched
the path; merge commits (>1 parent) and the root commit (no parent) are
attached as referenced (their first-parent diff does not represent a single
edit), and a pin whose sha no longer resolves is referenced with
degraded=True. Because a clean-file pin falls back to HEAD — a real
user commit whose diff is unrelated to the entry — each chain entry also carries
a provenance (memory for the isolated single-parent [memory] commit
:func:_git_auto_commit creates for a dirty pin, else other); the rolling
tip prefers a memory-provenance changed entry over an other one, so a
clean-file HEAD fallback can never hijack the tip away from a genuine memory
edit. (Genuine non-memory edits — reconstructed pins at historical user commits,
or a hand-written pin — remain changed, since their diff really is the edit.)
Known limitations (documented, not fixed here):
- Rename / move. The tether keys on
qualified_nameagainst the live graph, so a rename strands the old chain (reads as a new symbol). The FILE- level chain is always returned when the anchor's file resolves, so provenance is never fully lost. (The related line-shift mislabel — a later edit shifting a symbol's lines away from its pin-commit hunk ranges — is FIXED for reparseable languages: :func:_historical_symbol_spanresolves the symbol's span at the pin commit; non-Python languages fall back to the live span.) - Unsupported languages by name. A bare symbol name in a language kglite
does not parse (e.g. Julia
.jl) has no graph node; :func:resolve_anchorfalls back to a bounded source scan and, when a UNIQUE file defines the name, returns a file-level anchor (elseabsentwith a hint). Passing the filepathdirectly is always the unambiguous route.
is_built_asset ¶
True when path lives under a built/generated asset directory.
Segment-based (not substring) so that a legitimate redistribute/ module
is not mistaken for dist/.
Source code in memory/code_context.py
nested_repo_prefixes ¶
Repo-relative prefixes of nested git repositories under root.
Any directory OTHER than root that contains its own .git is a
nested/vendored repository or clone — its files are not part of this repo's
source and must never enter the code graph. The motivating case: eval
sandboxes that clone the whole repo into dev/runs/.../clone-*/ (each a
full working copy with its own .git), which otherwise swamp the graph with
thousands of duplicate File nodes and wedge the dashboard. The kglite
builder walks the raw working tree and honors neither .gitignore nor
nested-repo boundaries, so the exclusion is applied downstream as a graph
filter (see :func:in_nested_repo).
Each returned prefix is forward-slashed and ends in / so a plain
path.startswith(prefix) test excludes the entire subtree. The walk prunes
built-asset and dot directories for speed and stops descending as soon as a
nested repo is found (so the clones themselves are never traversed).
Source code in memory/code_context.py
in_nested_repo ¶
True when path lives inside one of prefixes (a nested repo/clone).
Companion to :func:nested_repo_prefixes; prefixes are computed once per
graph build and reused across the file/symbol filter loops.
Source code in memory/code_context.py
resolve_anchor ¶
Resolve a user anchor to a stable code-graph identity.
An anchor is either a symbol (keyed by qualified_name) or a file
(keyed by repo-relative path). Resolution is deliberately forgiving —
query may be a full angelo.memory.server._pin_files qualified name, a
trailing server._pin_files fragment, a bare _pin_files name, or a file
path. Unsupported / unparsed files (e.g. Julia .jl) resolve to a FILE
anchor so provenance is never lost.
When a bare-name symbol query matches no graph node AND is not path-ish, and
a repo root is supplied, a BOUNDED source-file scan
(:func:_scan_source_files_for_definition) is the last resort: it recovers
unparsed-language symbols (e.g. Julia) that kglite never nodalized. A unique
defining file yields a FILE anchor (with resolved_via='name-scan'); zero
or several matches keep absent but add a hint (and, for several, the
usual candidate_count / ambiguity_note).
Returns a dict with kind in {"symbol", "file", "absent"} plus, when
resolved, qualified_name / name / type / file_path /
line_number / end_line. kind == "absent" means the anchor could
not be located in the current graph (e.g. a deleted symbol) — callers keep
the historical chain but flag the anchor missing.
Source code in memory/code_context.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 | |
callers ¶
Decision-shaped 1-hop callers of a (specific) symbol/file anchor.
Source code in memory/code_context.py
callees ¶
Decision-shaped 1-hop callees of a (specific) symbol/file anchor.
Source code in memory/code_context.py
with_ambiguity ¶
Propagate an ambiguous anchor's disambiguation hints into payload.
:func:resolve_anchor records candidate_count / ambiguity_note when a
bare name resolved deterministically but non-uniquely. This surfaces them on
the tool payload so the user sees the resolution was ambiguous. Keys are added
only when present, so unambiguous results stay uncluttered.
Source code in memory/code_context.py
blast ¶
blast(graph: Any, anchor: dict, depth: int = DEFAULT_BLAST_DEPTH, node_cap: int = DEFAULT_BLAST_NODES) -> dict
Reverse-dependency BFS: what breaks if this anchor changes.
Unions two dependency channels with SEPARATE budgets so the precise signal is never evicted by the coarse one:
- reverse
CALLS(precise) — a breadth-first walk from the anchor's specific seed node(s); distance-1 callers are gradedbreaks, deeper onesmaybe(relation="calls"). This channel is filled FIRST, in distance order, up to the fullnode_cap— so transitive call-path breakage always survives the cap. - reverse
IMPORTS(coarse) — files that import the anchor's package, appended AFTER calls as distance-1 file-level dependents gradedmaybe(relation="imports"), up to a SMALL separate cap (min(node_cap - calls, DEFAULT_IMPORT_CAP)). Package-grained (kglite's only import granularity), so it is a coarse superset that must not crowd out real callers on a hot package.
Built assets are excluded throughout; cycle-safety is via visited.
Source code in memory/code_context.py
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 | |
import_neighborhood ¶
File/module import neighborhood: what this file imports and who imports it.
kglite records IMPORTS as File -> Module at a coarse (top-level)
module grain, so the reverse ("who imports this") is resolved by the anchor
file's top-level package name rather than the exact module — useful, but
package-level, not symbol-level.
Source code in memory/code_context.py
latest_code_commit ¶
SHA of the newest commit that changed a tracked path OUTSIDE .memory/.
This is the code_head used to key the code graph and the per-anchor
tether cache — deliberately NOT plain git HEAD. The memory MCP server
auto-commits .memory/ bookkeeping frequently (often several times an
hour), and every such commit moves HEAD without changing a single line
of code. Keying on raw HEAD would invalidate the entire code cache on
each of those commits, so a large tree's tether warm could never outrun the
churn (the code-lens overlay would perpetually re-warm and never light up).
Anchoring to the last code commit makes the cache stable across the
bookkeeping commits while still invalidating the instant real code lands.
Implemented with git log -1 over ':(top)' minus every .memory/
tree: the (top) magic anchors the pathspecs at the repo root so the result
is independent of the process cwd, ':(top)' matches all files,
':(exclude,top).memory' drops the ROOT .memory/, and
':(exclude,glob)**/.memory/**' drops any NESTED .memory/ (e.g. the
per-clone memory stores under dev/runs/.../evalmem-*/.memory/ that the
study runners commit constantly) — those are all memory bookkeeping, not code.
Without the nested exclude, a commit touching only a nested .memory/ file
would advance code_head, needlessly triggering the whole-tree code-graph
reparse (and, pre-fix, orphaning the tether cache). A mixed commit that also
touched real code still counts. Both the dashboard route and the agent-facing
codebase(context) path call this so they share one cache key. Falls back to
plain HEAD when the pathspec query fails or the repo has no non-memory
commit yet, and returns None when there is no repo/root.
Source code in memory/code_context.py
build_anchor_context ¶
build_anchor_context(graph: Any, root: Path | None, anchor: dict, entries: Sequence[dict], caches: dict | None = None) -> dict
Derive the tip + provenance chain for a single anchor.
caches (optional) is a pass-level cache bundle — a dict with sub-dicts
meta / diff / blob (git-op memoization keyed by (path, sha))
and span (the historical-span cache). When a caller warms MANY anchors in
one pass (:func:collect_tethers / :func:backfill_tethers) it shares one
bundle across every anchor, so the per-(path, sha) git subprocesses
(_commit_meta, _run_git_diff_ranges, git show) run ONCE for a file
rather than once per symbol — the difference between a snappy warm and one
that re-diffs a 200-symbol file 200 times. When caches is None (the
standalone single-anchor agent context call) the caches are per-call and
the span cache is loaded from and saved to disk here exactly as before; when a
span cache is SHARED in, the caller owns its one-shot persistence.
entries is the pre-collected set of memory entries carrying pins (each a
dict with id / title / type / created_at / files); the
caller gathers them (already excluding discarded/merged entries) so this
function stays free of the memory graph. For each entry that pins the
anchor's file the relation is derived conservatively:
- a single-parent pin commit that touched the path is diffed against its
first parent. A symbol anchor is
changedwhen a changed range overlaps its span AS IT WAS AT THE PIN COMMIT — resolved by reparsing the file blob at that sha (:func:_historical_symbol_span), so a later edit that shifts the symbol's lines does not mislabel a genuinechangedasreferenced. Only Python is reparsed; other languages fall back to the liveline_number..end_linespan. A file anchor ischangedwhen the diff is non-empty; otherwisereferenced. - a merge commit (>1 parent) or the root commit (no parent) is
attached as
referencedand NOT diffed — a single first-parent diff does not faithfully represent one edit there. - a pin whose sha no longer resolves (gc/rebase/amend), or a single-parent pin
whose first-parent diff cannot be computed (git error, or a missing parent
object in a shallow/partial clone), is
referencedwithdegraded=True— distinct from both a genuine reference and a genuinely empty (clean) diff.
Each entry also carries provenance: memory for the isolated
single-parent [memory] commit :func:_git_auto_commit makes for a dirty
pin, else other. This is used only by :func:_select_tip, which prefers a
memory-provenance changed entry, so a clean-file HEAD fallback (an
unrelated user commit whose diff happens to touch the path) can never hijack
the tip from a genuine memory edit.
The FILE-level chain is returned whenever the anchor's file resolves (so a
symbol anchor still carries its file's history). The chain is append-only,
ordered by created_at.
Source code in memory/code_context.py
1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 | |
cached_anchor_context ¶
cached_anchor_context(graph: Any, root: Path | None, anchor: dict, entries: Sequence[dict], code_head: str | None, pin_index: dict[str, str] | None = None, caches: dict | None = None, file_tokens: dict[str, str] | None = None) -> dict
:func:build_anchor_context with a lazy per-anchor disk cache.
The cache lives under .angelo/code_tethers/ and is keyed by the anchor's
OWN file token (its blob sha at code_head — see :func:_anchor_cache_disk_key)
plus its OWN relevant pins (:func:_anchor_pin_signature) — NOT the global
memory signature and NOT the global code_head. This keeps the canonical
edit→record→context loop warm across unrelated memory writes AND across code
commits to OTHER files, while still invalidating the instant a pin on this
file changes, an entry is discarded, or THIS file's content moves. A read
never touches the git working tree (the cache dir is under the gitignored
.angelo/); any cache error degrades to a direct recompute — the cache is
disposable.
A :func:build_anchor_context that raises is caught and a degraded result
(:func:_degraded_anchor_payload) is returned TRANSIENTLY — never propagated,
but also never written to the durable disk cache. So one pathological anchor
can neither abort a warm nor wedge the dashboard in refining, yet a
TRANSIENT failure (git index.lock contention, an interrupted subprocess,
an OOM reparse) retries on the very next call rather than freezing an empty
chain under the durable (code_head + pins) key until HEAD/pins move. A
permanently-failing anchor is bounded instead by the route's
_TETHER_ATTEMPTS settle cap. Only a SUCCESSFUL build is cached (and
invalidated when the anchor's pins or code HEAD move).
pin_index is forwarded to the cache-key computation for an O(1) per-anchor
signature; the resulting key is byte-identical to the un-indexed path, so a
warm write and a later read agree whether or not the index was supplied.
caches is the pass-level git/span cache bundle forwarded to
:func:build_anchor_context on a cold build (shared by a warm pass so a
file's git ops resolve once, not once per symbol).
Source code in memory/code_context.py
2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 | |
backfill_tethers ¶
backfill_tethers(graph: Any, root: Path | None, entries: Sequence[dict], code_head: str | None) -> dict
Optional one-shot: warm the tether cache for every pinned anchor.
Walks every entry pin once, inverts each to touched anchors, and writes the
per-anchor cache entries. Not required for correctness — lazy
:func:cached_anchor_context covers cold anchors — but pays the invert cost
up front for a large existing tree. Returns a small progress summary.
Source code in memory/code_context.py
tether_record ¶
The compact overlay dict for one anchor, or None if it has no tether.
Distills a :func:build_anchor_context payload down to what the code-lens
overlay needs::
{"anchor": <qualified_name | file_path::name | file_path>,
"kind": "symbol" | "file",
"file_path": <repo-relative path>,
"tip": <the _select_tip result or None>,
"tip_created_at": <the tip entry's created_at, for latest-activity ranking>,
"chain_count": int,
"relation": <the anchor's governing (tip) relation, or None>,
"degraded": bool}
The anchor id is the overlay identity (:func:_overlay_anchor_id) so it
matches the backdrop node id even for a null-qualified_name symbol.
Returns None for an empty chain (the anchor renders grey from the
backdrop, not the lit overlay). Factored out so both :func:collect_tethers
and the dashboard's single-pass cold-start build the record identically.
Source code in memory/code_context.py
peek_anchors_parallel ¶
peek_anchors_parallel(anchors: Sequence[dict], entries: Sequence[dict], code_head: str | None, pin_index: dict[str, str] | None = None, root: Path | None = None, file_tokens: dict[str, str] | None = None, *, workers: int | None = None) -> list[tuple[dict, dict | None]]
Peek every anchor's disk cache in parallel; return [(anchor, payload)].
:func:_peek_anchor_context is READ-ONLY (reads one cache file, validates its
key against pin_index) with no shared mutable state, so it is safe to fan
out across a thread pool. A cache miss is a cheap stat; a hit reads+parses
a small JSON file — and a warm reload peeks THOUSANDS of hits, the dominant
cost of a fresh process's first overlay pass. Parallelizing the reads cuts that
wall time by roughly the pool size (I/O-bound on a synced filesystem). Order is
preserved so callers can zip results back against anchors. Never raises per
anchor: a peek that errors is reported as a miss (None).
Source code in memory/code_context.py
collect_tethers ¶
collect_tethers(graph: Any, root: Path | None, entries: Sequence[dict], code_head: str | None, *, cache_only: bool = False) -> list[dict]
Returning variant of :func:backfill_tethers: the tether overlay data.
Where :func:backfill_tethers only warms the cache, this returns a
:func:tether_record for every pinned anchor that carries at least one
tether. The per-anchor path is IDENTICAL to the agent-facing context
action (:func:cached_anchor_context → :func:build_anchor_context →
:func:_select_tip), so the tip logic never diverges.
cache_only (cold-start): peek the per-anchor disk cache and include ONLY
already-warm anchors, never computing a cold invert. The dashboard serves this
partial overlay instantly while a background thread warms the full set.
Two-phase for a snappy WARM (reload) path: phase 1 peeks every anchor's disk
cache IN PARALLEL (:func:peek_anchors_parallel) — reads are I/O-bound and
independent, so a fully-warm cache (a fresh process re-reading thousands of
files) resolves in a fraction of the old serial time. phase 2 builds only the
cold misses serially (they mutate the shared parse/span caches, so they are not
parallelized), priming the git caches once if there is anything to build. In
the all-warm case phase 2 is empty; in the cold case phase-1 peeks are cheap
misses and the build path is unchanged.
Each anchor is wrapped independently so one raising anchor can never abort the
whole warm (leaving the rest of the invert — and the dashboard's refining
settle — permanently stuck). cached_anchor_context degrades a raising
anchor transiently (returned, not cached, so it retries next call); this guard
also covers _peek_anchor_context / tether_record.
Source code in memory/code_context.py
2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 | |
ownership ¶
Who authored an anchor: git-blame attribution over its live line span.
For a symbol anchor, blames its [line_number, end_line] range in its
file; for a file anchor (or a symbol lacking a line span), blames the
whole file. Contributions are aggregated by author into owners — line
count and the author's most-recent commit (sha + date) within the blamed
span — ranked by lines desc. The memory-mcp bot is filtered out (by its
memory@local email — unspoofable — with the author name as a secondary
check) so ownership reflects real authoring contributors.
Coordinate consistency (FU-4 fix): the -L range comes from the LIVE anchor,
which kglite parses from the WORKING TREE, so blame is run against the working
tree too (no HEAD argument). Pinning to HEAD while ranging with
working-tree line numbers would silently misattribute lines whenever the tree
is dirty and the symbol shifted; blaming the working copy keeps the line
numbers aligned with the anchor. On a clean tree this is identical to blaming
HEAD; on a dirty tree, locally-modified lines attribute to git's all-zero
Not Committed Yet boundary. Those uncommitted lines are NOT committed
authorship, so they are excluded from owners (an informational
uncommitted_lines count is surfaced instead) — owners contains only
real committers, and the memory-mcp bot is likewise filtered.
Robust by construction: the file path handed to git is the resolved
anchor's file_path (never user free-text), git is invoked with an argv
list (no shell), and the no-git / not-a-repo / unresolvable-file / empty-blame
cases return an informative note rather than raising.
Source code in memory/code_context.py
2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 | |
health ¶
Graded code-health report with located, actionable drivers.
Reuses degree-style centrality already latent in the code graph (method counts, call fan, import degree, call-cycle SCCs) plus git churn to surface five anti-pattern signals: god object, coupling, cycle, churn, dead code. Betweenness/pagerank over the full multi-hundred-thousand-node graph is deliberately avoided to keep the call sub-second; degree centrality captures the same anti-patterns cheaply.