English | 简体中文
npx skills add lo2589/Code-Dendrite # 3 agent skills: understand · learn · rebuild
pip install git+https://github.com/lo2589/Code-Dendrite # the cdd command (Python ≥ 3.10, no dependencies)Then tell your agent:
Use cdd-understand to show me how <your entry function> works in this repo.
The same coding task is shown side by side: rg on the left and cdd on the right. The animation plays directly in this README.
Static analysis instead of vector search.
Retrieval finds code that looks alike. Navigation needs code that is connected. A function that calls yours looks nothing like yours in text — which is exactly where similarity search is bound to fail on call-chain questions. CodeDendrite doesn't guess at similarity: it parses a repository into a graph of causal edges and lets you walk along them.
| Item | Value |
|---|---|
| Layout | Five layers under src/; src/cdd_mcp/ is the MCP server; skills/ sits next to src/ |
| Requirements | Python ≥ 3.10; the core uses the standard library only |
| Parsing | Python through the standard-library ast module; every other language through lightweight, dependency-free pattern-matching adapters, so their symbols and call edges are heuristic. Depth per language: language support matrix |
| Index | Not committed to git. After cloning, run cdd rebuild first; see counts with cdd count / cdd status |
Option 1: run from source (no install)
git clone https://github.com/lo2589/Code-Dendrite
cd Code-Dendrite
python3 src/usage/cli/cdd rebuild . --virtual # build the index first: every read command except peek needs it
python3 src/usage/cli/cdd countsrc/usage/cli/cdd puts src/ on the subprocess PYTHONPATH by itself; no extra setup is needed.
Option 2: install as the cdd command
pip install . # use pip install -e . for development
cdd rebuild /path/to/project --virtualOptional dependencies (declared in pyproject.toml):
pip install '.[mcp]' # the MCP server needs the mcp package
pip install '.[test]' # running the tests needs pytestMCP server: once mcp is installed, add this to .mcp.json:
{
"codedendrite": {
"command": "cdd-mcp",
"args": ["--root", "/path/to/project"]
}
}The project that --root points to must have been indexed with cdd rebuild.
lens plugin (optional, only useful with DeepSeek Harness): lens/scripts/install.sh. Override the harness repo path, profile and port with DSH_REPO, PROFILE and DSH_PORT. The node path and the harness restart script inside the script are hard-coded to the author's machine; edit those two lines before using it elsewhere.
| Path | Contents |
|---|---|
src/scanner/ |
Language scanner: source → symbols / line numbers / imports / call sites. language/ (23 extensions routed by file extension across 23 LanguageSpecs — actual capability depth per spec is measured by the language support matrix below; 17 specs reach call-edge depth, 3 stop at symbol detection, 3 are recognized but not parsed), calls/, hashing/, project/, store/, structure/ |
src/analyzer/ |
Analyzer: symbols → anchor identity + causal edges. identity/ (structural hashing and drift), anchors/, inherit/, dataflow/, dynamic/, path/, control_flow/, ui/, shared/ |
src/api/ |
Query surface: inspector/, navigator/, query/, relations/ |
src/usage/ |
Usage surface: cli/, packets/, workflow/, profiles/, tools/, repair/, structure/, analyze/ |
src/view/ |
Visualization: browser/, code_map/, relations/, timeline/, module_tree/, pipeline/, html/ |
lens/ |
DSH plugin dsh-cdd-lens: a session trajectory sidebar + an interactive anchor graph (a node package, independent of src/) |
lens-pcb/ |
A parallel branch of lens (PCB-style layout) with its own build stamp |
src/cdd_mcp/ |
MCP server: exposes the layers above as tools |
skills/ |
Three agent skills: cdd-understand (understand a project), cdd-rebuild (topology refactoring), cdd-learn (layer-by-layer guided learning) |
src/tests/ |
Tests and fixtures |
Dependency direction: scanner → analyzer → api → usage / view. scanner and analyzer import nothing above them. One reverse dependency remains: api/query/cmd_chain.py imports view.relations, view.timeline, view.code_map and usage.profiles.yaml_profile inside functions (4 imports). Modules that used to sit in the wrong layer have moved, so scan_project, debug_workflow, generate_module_tree, smart_analyze and the navigator (start, jump, …) are imported from usage, and generate_mermaid_for_project from view.html.graph, not from api.
The entry point is src/cdd_mcp/server.py::call_tool. Once the index is built, one command drills it down to the implementation layer:
python3 src/usage/cli/cdd chain "src/cdd_mcp/server.py::call_tool" --depth 5The chain crosses three layers: mcp → usage → analyzer. The output is a tree expansion: a function called from several places is printed once at each place, so "lines" and "distinct functions" are two different numbers:
| Layer | Lines | Distinct functions |
|---|---|---|
src/cdd_mcp/ |
15 | 10 |
src/usage/ |
36 | 13 |
src/analyzer/ |
4 | 1 |
| Total | 55 | 24 |
The most repeated function is _anchor_attr (14 times). Full output (all 55 lines):
Chain from 'src/cdd_mcp/server.py::call_tool' (55 entries, direction=down):
[0] call_tool src/cdd_mcp/server.py:294
[1] tool_context src/cdd_mcp/server.py:84
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] _relations src/cdd_mcp/server.py:54
[2] _module_tree src/cdd_mcp/server.py:62
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[2] build_level0 src/usage/packets/task_context_packet/anchor.py:275
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[2] build_level1 src/usage/packets/task_context_packet/anchor.py:336
[3] _find_anchor_area src/usage/packets/task_context_packet/anchor.py:328
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _find_anchor_module src/usage/packets/task_context_packet/plugin.py:123
[2] build_level2 src/usage/packets/task_context_packet/anchor.py:422
[3] _build_edge_indexes src/usage/packets/task_context_packet/anchor.py:250
[3] _score_related_symbol src/usage/packets/task_context_packet/anchor.py:391
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _find_anchor_module src/usage/packets/task_context_packet/plugin.py:123
[1] tool_symbol src/cdd_mcp/server.py:112
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] _relations src/cdd_mcp/server.py:54
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[2] build_anchor_packet src/usage/packets/task_context_packet/anchor.py:622
[3] _build_edge_indexes src/usage/packets/task_context_packet/anchor.py:250
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[1] tool_search src/cdd_mcp/server.py:126
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[2] resolve_task_anchor src/usage/packets/task_context_packet/anchor.py:113
[3] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _bounded_matches src/usage/packets/task_context_packet/anchor.py:78
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[3] _match_sort_key src/usage/packets/task_context_packet/anchor.py:68
[4] _anchor_attr src/usage/packets/task_context_packet/anchor.py:54
[4] _is_test_file src/usage/packets/task_context_packet/plugin.py:119
[1] tool_impact src/cdd_mcp/server.py:150
[2] _relations src/cdd_mcp/server.py:54
[2] _anchors src/cdd_mcp/server.py:47
[3] load_anchors src/analyzer/anchors/core_load.py:33
[1] tool_modules src/cdd_mcp/server.py:194
[2] _project_summary src/cdd_mcp/server.py:73
This chain is computed from the index, not copied by hand: the numbers above come from the index as of 2026-09-14. Running the same command should give the same starting point and the same set of files; line counts, function counts and line numbers change with the code.
cdd is src/usage/cli/cdd (a Python script without an extension, run via its shebang). The examples below run it from source; after installing, replace python3 src/usage/cli/cdd with cdd.
cd <repo>
python3 src/usage/cli/cdd status # index location, counts, timestamps
python3 src/usage/cli/cdd rebuild . --virtual # full rebuild
python3 src/usage/cli/cdd update . # incremental update; builds fully the first time
python3 src/usage/cli/cdd count # count of each causal edge type
python3 src/usage/cli/cdd peek <file> # parse one file on the spot, no index needed
python3 src/usage/cli/cdd chain <anchor> --depth 12
python3 src/usage/cli/cdd callers <anchor> # upward: who uses it
python3 src/usage/cli/cdd chain2context <anchor> --budget 40000 --output ctx.md
python3 src/usage/cli/cdd draw # generate the browser-based directory explorer
python3 src/usage/cli/cdd modules <keyword> # semantic moduleschain2context assembles every anchor on a chain, together with its source code, into context ready to feed to a model. Anchors that exceed the budget are listed in a separate section instead of being silently dropped.
Counts vary with the code and the scan scope; use cdd count for current values.
| Type | Meaning |
|---|---|
call |
A calls B |
dataflow |
A's output flows into B's input |
impact |
≥2 modules depend on the same target, so changing it affects all of them |
inherit |
A subclass inherits from a parent class |
dispatch |
Registry / decorator / HTTP route dispatch |
Edges are built only when they can be resolved statically. Nothing is invented for what can't be resolved: when cdd callers finds zero callers for a method, it reports the unresolved candidate call sites instead of claiming "nobody uses it".
The scanner routes 23 file extensions through src/scanner/language/registry.py. They are not all parsed at the same depth — capability is split into three tiers, each measured by src/tests/tests/test_lang_support_matrix_v1_48.py against the fixtures in src/tests/fixtures/lang_matrix/. The matrix table below is guarded by a drift test (test_readme_matrix_matches_fixtures): changing a tier requires updating the test in lock-step.
| Tier | Capability | Specs (count) | What you get |
|---|---|---|---|
| T1 | Symbols + call edges | 17 | parse_paths yields ≥2 anchors per fixture; the caller anchor records a helper callee |
| T2 | Symbols only (generic / component) | 3 | ≥1 anchor; no call-edge guarantee |
| T3 | Recognized but not parsed | 3 | language_for_path returns a spec; parse_paths produces zero anchors (yaml / json / toml — they exist so the indexer doesn't trip on them, but the analyzer is None) |
| Tier | Spec | Fixture | Anchor count (measured) |
|---|---|---|---|
| T1 | python | a.py |
2 |
| T1 | typescript | b.ts |
2 |
| T1 | javascript | c.js |
2 |
| T1 | cpp | d.cpp |
2 |
| T1 | java | e.java |
3 |
| T1 | kotlin | f.kt |
2 |
| T1 | go | g.go |
2 |
| T1 | rust | h.rs |
2 |
| T1 | swift | i.swift |
2 |
| T1 | php | j.php |
2 |
| T1 | ruby | k.rb |
2 |
| T1 | html | l.html |
2 |
| T1 | csharp | m.cs |
3 |
| T1 | shell | p.sh |
2 |
| T1 | powershell | q.ps1 |
2 |
| T1 | vue | r.vue |
2 |
| T1 | svelte | s.svelte |
2 |
| T2 | scala | n.scala |
1 |
| T2 | objective-c | o.m |
3 |
| T2 | sql | t.sql |
2 |
| T3 | yaml | u.yaml |
0 |
| T3 | json | v.json |
0 |
| T3 | toml | w.toml |
0 |
When you change an adapter's depth — adding call edges to scala, teaching the indexer about yaml structure, splitting cpp into a real parser — the test file's tier table is the contract: move the spec, fix the fixture, and the matrix here follows.
Anchors go down to module-level defs and class methods only. A def nested inside a function body (a local helper, a closure factory) does not get its own anchor: its call edges are recorded under the enclosing function's callees, and opening the enclosing function's source gives you all of it — there is no separate identity worth querying. Three examples of this rule:
_coerce_callers/_coerce_calleesinsidesrc/analyzer/anchors/core_load.py::load_anchors— the index has no anchor entries for them, butload_anchors.calleeskeeps string traces of them_relatedinsidesrc/usage/packets/task_context_packet/anchor.py::build_anchor_packet— same as above- This is not a missed detection; it is a design choice: the anchor generator only walks
tree.body(module level + class-method level)
An Add/Search service for the Agent Memory Leaderboard Coding track. Its only model is text-embedding-v4: it makes no LLM calls and uses no reranker, and it never clones a repository.
Add receives request_id, user_id, session_id, messages[{role, content, timestamp?}], one solved coding task per request. It processes the task in these steps:
- Fingerprint the trajectory. A trajectory already stored for another
user_idcosts only a reference row. - Parse the messages into events: the issue, thinking, tool calls and their results. Results that were split across messages are glued back together.
- Replay the files in time order.
Readrecords the numbered lines,Editappliesold_string → new_stringand records where it landed, andWritereplaces the file. - Extract anchors (
file::Class.methodwith line spans) with Pythonast, together with same-file call edges. Files that were only read in pieces fall back todef/classlines. - Link the task's
read/edit/test/ qualifiedmentionevents to those anchors. - Build one unit per touched anchor, holding the code, its calls and callers, and the task's diff (at most about 1,500 tokens). Everything else becomes 100-word chunks with a stride of 80.
- Embed only the plain chunks, then write everything in one transaction.
Add answers 200 only after the write is durable. The same request_id with the same body is a no-op; with a different body it returns 409. Caps: 2 MB of replayed code and 2,000 anchors per trajectory, and 503 when less than 3 GB of disk is free.
Search receives query, user_id, top_k. It searches only inside the trajectories that this user_id references, through three routes: exact symbol and path hits (weight 3), BM25 over all units, and vector similarity over the plain chunks. The routes are fused by weighted reciprocal rank. Each anchor appears once. Other tasks that touched the same symbol are listed as pointers only: newest first, without their code, and using at most 20% of the budget. A reply holds at most top_k items and about 12k tokens, as {"data": [{id, content, score, created_at}]}. It returns evidence only and never a generated answer.
mkdir -p ~/.config/cdd-aml && (umask 077; cat > ~/.config/cdd-aml/dashscope.key) # Model Studio key
cd src && python3 -m usage.aml_coding serve --data ~/cdd-aml-data --key-file <file with the AML API key> --port 8014
python3 -m usage.aml_coding stats --data ~/cdd-aml-data # calls, embedding tokens, costOrigin: the service is the author's own original work and is built on CodeDendrite's anchor model. No third-party code is reused. The only runtime dependency is numpy, and the only model called is text-embedding-v4.
cd <repo>
pip install '.[test]'
PYTHONPATH=src python3 -m pytest src/tests src/tests/tests -qThese are build outputs. Don't edit them by hand; regenerate them with cdd rebuild:
.codedendrite/— anchors.json / relations.json / module_tree*.json / various .html files.code_genome_db.json,.code_hash_db.json(with.lockfiles)
.codedendrite_ignore controls what the scan excludes (currently .cache/, backup/, doc/, the lib/ build output and design/ mockups of lens/ and lens-pcb/, and lens/output/).
