@nanonets/graft 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -92
- package/dist/ai/llm/types.d.ts +2 -2
- package/dist/ai/llm/types.js +1 -1
- package/dist/ask/ask.d.ts +14 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +27 -7
- package/dist/ask/ask.js.map +1 -1
- package/dist/claude/format.d.ts +1 -2
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +23 -23
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +25 -1
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +13 -6
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +132 -80
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/cli-epilogue.js +1 -1
- package/dist/cli-epilogue.js.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -1
- package/dist/context/check.d.ts +21 -0
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +32 -1
- package/dist/context/check.js.map +1 -1
- package/dist/context/node-file.d.ts +10 -0
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +34 -1
- package/dist/context/node-file.js.map +1 -1
- package/dist/context/savings.d.ts +6 -0
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +9 -1
- package/dist/context/savings.js.map +1 -1
- package/dist/graph/build.d.ts.map +1 -1
- package/dist/graph/build.js +4 -1
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +16 -6
- package/dist/graph/traverse-cli.js.map +1 -1
- package/dist/graph/workspace-cli.d.ts.map +1 -1
- package/dist/graph/workspace-cli.js +5 -1
- package/dist/graph/workspace-cli.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +9 -5
- package/dist/mcp/tools.js.map +1 -1
- package/dist/search/grep-cli.js +1 -1
- package/dist/search/grep-cli.js.map +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<!-- <img src="assets/graft.png" alt="Graft" width="220"/> -->
|
|
3
|
+
<img src="assets/graft-hero.png" alt="Graft — open-source context layer for large codebases" width="100%"/>
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
**Your coding agent reads 30 files to change 3.**
|
|
9
|
-
**Graft gives it the map it should have read first.**
|
|
5
|
+
### Turbocharge Claude Code, Cursor, Codex, Gemini & every coding agent: faster, cheaper, with contextual understanding specific to your codebase.
|
|
10
6
|
|
|
11
7
|
<p>
|
|
8
|
+
<a href="https://graft.nanonets.ai"><img src="https://img.shields.io/badge/website-graft.nanonets.ai-546FFF?style=for-the-badge" /></a>
|
|
12
9
|
<a href="https://www.npmjs.com/package/@nanonets/graft"><img src="https://img.shields.io/npm/v/%40nanonets%2Fgraft?style=for-the-badge&logo=npm&logoColor=white&label=npm" /></a>
|
|
13
10
|
<a href="https://www.npmjs.com/package/@nanonets/graft"><img src="https://img.shields.io/npm/dm/%40nanonets%2Fgraft?style=for-the-badge&logo=npm&logoColor=white&label=downloads" /></a>
|
|
14
11
|
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/%40nanonets%2Fgraft?style=for-the-badge&logo=nodedotjs&logoColor=white" /></a>
|
|
15
12
|
<img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=for-the-badge&logo=typescript&logoColor=white" />
|
|
16
13
|
<img src="https://img.shields.io/badge/License-MIT-20C997?style=for-the-badge" />
|
|
17
14
|
<img src="https://img.shields.io/badge/telemetry-none-546FFF?style=for-the-badge" />
|
|
15
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/NanoNets/Graft"><img src="https://img.shields.io/ossf-scorecard/github.com/NanoNets/Graft?style=for-the-badge&label=openssf%20scorecard" /></a>
|
|
18
16
|
</p>
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
### Up to **4× cheaper** and **3× faster**, with better or no loss of correctness.
|
|
19
|
+
|
|
20
|
+
| vs. a standard session, no Graft | With Graft |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Tool calls | **46% fewer** |
|
|
23
|
+
| Tokens | **42% fewer** |
|
|
24
|
+
| Time | **60% less** |
|
|
25
|
+
| Correctness | **equal** |
|
|
26
|
+
|
|
27
|
+
<sub>The table is a 162-run controlled benchmark (same agent, same file tools, only the context differs). The "up to 4× cheaper / 3× faster" figures are the biggest single-task wins from a separate real-repo sweep (PocketBase, ollama, Excalidraw). [Full method & per-repo numbers ↓](#tested-on-your-popular-repos)</sub>
|
|
22
28
|
|
|
23
29
|
</div>
|
|
24
30
|
|
|
@@ -28,6 +34,26 @@
|
|
|
28
34
|
|
|
29
35
|
---
|
|
30
36
|
|
|
37
|
+
## Contents
|
|
38
|
+
|
|
39
|
+
- [Quick start](#quick-start)
|
|
40
|
+
- [The problem](#the-problem)
|
|
41
|
+
- [What Graft does](#what-graft-does)
|
|
42
|
+
- [How the graph gets built](#how-the-graph-gets-built)
|
|
43
|
+
- [What's in a node](#whats-in-a-node)
|
|
44
|
+
- [What runs where](#what-runs-where)
|
|
45
|
+
- [Agent integration](#agent-integration) — [MCP server](#mcp-server) · [Claude Code (deep integration)](#claude-code-deep-integration)
|
|
46
|
+
- [CLI](#cli)
|
|
47
|
+
- [Search & orient](#search--orient-graft-grep--graft-map) (`graft grep` / `graft map`)
|
|
48
|
+
- [Monorepos & multi-repo folders](#monorepos--multi-repo-folders)
|
|
49
|
+
- [Visualize it](#visualize-it-graft-viz) (`graft viz`)
|
|
50
|
+
- [Benchmark](#benchmark)
|
|
51
|
+
- [Tested on your popular repos](#tested-on-your-popular-repos)
|
|
52
|
+
- [Development](#development)
|
|
53
|
+
- [License](#license)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
31
57
|
## Quick start
|
|
32
58
|
|
|
33
59
|
```bash
|
|
@@ -37,64 +63,16 @@ graft init # build the graph + wire it into Claude Code
|
|
|
37
63
|
|
|
38
64
|
That is the whole setup. `graft init` builds `graft/` from your code and drops a statusline and hooks into `.claude/`, so from the next session on Graft rides along in Claude Code: it pulls the matching nodes into each prompt and rebuilds the graph in the background after every turn. No daemon, no re-indexing to remember, nothing to run or maintain by default — the graph is just files.
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
`graft build` adds `graft/` to your `.gitignore` automatically — the graph is a local, regenerable cache (like `node_modules`), not something you commit. What you share is the wiring `init` dropped into `.claude/`; each teammate runs `graft build` to generate their own graph:
|
|
41
67
|
|
|
42
68
|
```bash
|
|
43
|
-
git add
|
|
69
|
+
git add .claude && git commit -m "wire in graft"
|
|
44
70
|
```
|
|
45
71
|
|
|
46
72
|
Prefer not to install globally? `npx @nanonets/graft init` works the same way.
|
|
47
73
|
|
|
48
74
|
---
|
|
49
75
|
|
|
50
|
-
## Agent integration
|
|
51
|
-
|
|
52
|
-
One command wires Graft into the coding agents you use:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npx @nanonets/graft init
|
|
56
|
-
# detects your agents and writes each one's native instruction file;
|
|
57
|
-
# Claude Code additionally gets the live statusline + hooks below
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`init` auto-detects which agents are present (via their config directories) and writes a marker-fenced Graft section into each one's shared instruction file — `AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md` — or a wholly-owned rule file for the agents that use one — `.cursor/rules/graft.mdc`, `.kiro/steering/graft.md`, `.windsurf/rules/graft.md`. Re-running only updates Graft's own section (or replaces the owned file) and never touches the rest of your content.
|
|
61
|
-
|
|
62
|
-
| Flag | Effect |
|
|
63
|
-
|---|---|
|
|
64
|
-
| `--agents <ids...>` | wire only these — ids: `agents`, `cursor`, `gemini`, `copilot`, `kiro`, `windsurf`, `claude` |
|
|
65
|
-
| `--all-agents` | write instruction files for every known agent, detected or not |
|
|
66
|
-
| `--no-agents` | Claude Code wiring only; skip other agents |
|
|
67
|
-
| `--list-agents` | print the known agent ids and exit |
|
|
68
|
-
| `--no-mcp` | skip MCP server registration |
|
|
69
|
-
| `--no-hooks` | skip hook installation |
|
|
70
|
-
|
|
71
|
-
### MCP server
|
|
72
|
-
|
|
73
|
-
`graft init` also registers Graft's MCP server with agents that support it, so
|
|
74
|
-
`graft_ask`, `graft_callers`, `graft_grep`, `graft_skeleton`, `graft_map`, and
|
|
75
|
-
`graft_check` appear as native tools — no shell required. Claude Code now gets
|
|
76
|
-
this too: `graft init` writes the server into the project's `.mcp.json` (restart
|
|
77
|
-
Claude Code to load it). Skip with `--no-mcp`. Run it manually with
|
|
78
|
-
`graft mcp [dir]`, or register it by hand:
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{ "mcpServers": { "graft": { "command": "npx", "args": ["-y", "@nanonets/graft", "mcp"] } } }
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Where a CLI agent supports user-level `hooks.json`, `init` also installs Graft's post-edit hook — blast-radius warnings and automatic `$0` graph re-sync after edits (skip with `--no-hooks`).
|
|
85
|
-
|
|
86
|
-
### Claude Code (deep integration)
|
|
87
|
-
|
|
88
|
-
`graft init` always wires up Claude Code, and Claude Code gets more than an instruction file. From then on, any Claude Code session opened in the repo gets:
|
|
89
|
-
|
|
90
|
-
- **a live statusline** — graph size, % enriched, and a `⚠ N stale` warning when the code has moved ahead of the graph
|
|
91
|
-
- **auto-sync** — after you edit code, Graft rebuilds the graph in the background at the end of the turn (structural, `$0` — it never calls the LLM on its own)
|
|
92
|
-
- **context on tap** — each prompt pulls the matching nodes into the session; editing a file surfaces what depends on it ("blast radius"); new sessions start with the repo map
|
|
93
|
-
|
|
94
|
-
`graft init` is idempotent and never clobbers your existing `.claude/settings.json` — it merges its blocks and leaves the rest alone. Want the LLM summaries too? Run `graft build --deep` (with a key) whenever you like; auto-sync will never do it for you.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
76
|
## The problem
|
|
99
77
|
|
|
100
78
|
Every task, your coding agent starts blind. Before it changes anything, it re-explores the repo: grep a term, open a file, follow an import, back out, try again. It is rebuilding a picture of a codebase it mapped an hour ago and threw away. That rediscovery burns most of a run's tool calls, tokens, and latency, and it is pure overhead:
|
|
@@ -105,6 +83,10 @@ Every task, your coding agent starts blind. Before it changes anything, it re-ex
|
|
|
105
83
|
|
|
106
84
|
Humans onboard to a codebase once. Agents onboard every single time.
|
|
107
85
|
|
|
86
|
+
<p align="center">
|
|
87
|
+
<img src="assets/graft-site-act-demo.gif" alt="A no-map agent's exploration trail wandering file to file before it finds what it needs" width="820"/>
|
|
88
|
+
</p>
|
|
89
|
+
|
|
108
90
|
---
|
|
109
91
|
|
|
110
92
|
## What Graft does
|
|
@@ -126,7 +108,16 @@ Graft builds the graph in two passes, both powered by a language model:
|
|
|
126
108
|
1. **Read each file.** Every source file is summarized once into a short description of what it does.
|
|
127
109
|
2. **Group into nodes.** Those summaries are grouped into a curated set of nodes (subsystems, key files, and concepts) with typed links between them. Graft chooses the right level of detail for you instead of making one node per file, so a big repo becomes a few dozen readable nodes.
|
|
128
110
|
|
|
129
|
-
|
|
111
|
+
```mermaid
|
|
112
|
+
flowchart LR
|
|
113
|
+
S[Source files] --> T["Tier 1 — tree-sitter<br/>no model, no key"]
|
|
114
|
+
S --> P1["Pass 1 — LLM summarizes<br/>each file (--deep)"]
|
|
115
|
+
T --> W["graft/.graph/wiring.json<br/>per-symbol code graph"]
|
|
116
|
+
P1 --> P2["Pass 2 — group into nodes<br/>+ typed links"]
|
|
117
|
+
P2 --> N["graft/*.md<br/>markdown node graph"]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Both LLM passes are cached by content hash. Re-running only touches the files that changed, so the second build is fast and cheap.
|
|
130
121
|
|
|
131
122
|
Alongside the markdown graph, `graft build` builds `graft/.graph/wiring.json` — a per-symbol code graph — plus a per-file wiring card mirroring your source tree. Tier 1 is pure tree-sitter (every function, class, and call edge; deterministic, no model, no network), which is why plain `graft build` needs no key. The `--deep` pass adds a one-line summary and a crux excerpt per symbol, cached by body hash.
|
|
132
123
|
|
|
@@ -154,29 +145,72 @@ _Summary, sources, links, and notes ship today in markdown nodes. The crux ships
|
|
|
154
145
|
|
|
155
146
|
---
|
|
156
147
|
|
|
157
|
-
##
|
|
148
|
+
## What runs where
|
|
149
|
+
|
|
150
|
+
- **On your machine, no key, no network:** the structural code graph. `graft build` (wiring graph + per-file cards), `graft check`, and `graft ask` are deterministic tree-sitter — they never call a model.
|
|
151
|
+
- **Through your provider key:** the LLM-written parts — `graft build --deep` adds the concept nodes (file summaries + node synthesis) and the per-symbol summaries and cruxes. graft is vendor-neutral: set `GRAFT_PROVIDER` (`openai` for any OpenAI-compatible endpoint, or `anthropic` for the native API), your `GRAFT_API_KEY`, `GRAFT_MODEL`, and — for the `openai` wire format — `GRAFT_BASE_URL` to point at OpenRouter, Fireworks, Groq, a LiteLLM proxy, a local server, or OpenAI itself. Or pass `--provider/--model/--api-key/--base-url` on the command line. (`OPENROUTER_API_KEY` still works as a deprecated fallback.)
|
|
152
|
+
- **No telemetry** and no analytics — the only network calls are the LLM requests you configured.
|
|
153
|
+
|
|
154
|
+
See [`.env.example`](.env.example) for the full list of settings (model, base URL, graph directory).
|
|
155
|
+
|
|
156
|
+
---
|
|
158
157
|
|
|
159
|
-
|
|
158
|
+
## Agent integration
|
|
159
|
+
|
|
160
|
+
One command wires Graft into the coding agents you use:
|
|
160
161
|
|
|
161
162
|
```bash
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
npx @nanonets/graft init
|
|
164
|
+
# detects your agents and writes each one's native instruction file;
|
|
165
|
+
# Claude Code additionally gets the live statusline + hooks below
|
|
164
166
|
```
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
`init` auto-detects which agents are present (via their config directories) and writes a marker-fenced Graft section into each one's shared instruction file — `AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md` — or a wholly-owned rule file for the agents that use one — `.cursor/rules/graft.mdc`, `.kiro/steering/graft.md`, `.windsurf/rules/graft.md`. Re-running only updates Graft's own section (or replaces the owned file) and never touches the rest of your content.
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
| Flag | Effect |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `--agents <ids...>` | wire only these — ids: `agents`, `cursor`, `gemini`, `copilot`, `kiro`, `windsurf`, `claude` |
|
|
173
|
+
| `--all-agents` | write instruction files for every known agent, detected or not |
|
|
174
|
+
| `--no-agents` | Claude Code wiring only; skip other agents |
|
|
175
|
+
| `--list-agents` | print the known agent ids and exit |
|
|
176
|
+
| `--no-mcp` | skip MCP server registration |
|
|
177
|
+
| `--no-hooks` | skip hook installation |
|
|
170
178
|
|
|
171
|
-
|
|
179
|
+
### MCP server
|
|
172
180
|
|
|
173
|
-
|
|
181
|
+
`graft init` also registers Graft's MCP server with agents that support it, so these six tools appear natively, no shell required. Claude Code gets this too: `graft init` writes the server into the project's `.mcp.json` (restart Claude Code to load it). Skip with `--no-mcp`; run it manually with `graft mcp [dir]`.
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
| Tool | Takes | What it's for |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `graft_ask` | a question | Ranked nodes with file:line, source inlined — usually the full answer, no follow-up read needed. |
|
|
186
|
+
| `graft_skeleton` | a file path | Every signature in that file, no bodies — the API surface for a tenth of the tokens. |
|
|
187
|
+
| `graft_callers` | a symbol | Who depends on it, or what it depends on with `direction: out`, N levels deep for blast radius. |
|
|
188
|
+
| `graft_grep` | a regex | Every hit, grouped by enclosing symbol, ranked by how coupled that symbol is. |
|
|
189
|
+
| `graft_map` | nothing | A first look at an unfamiliar repo: directory clusters, hubs, hotspots. |
|
|
190
|
+
| `graft_check` | nothing | Whether the local graph has drifted from the code. |
|
|
178
191
|
|
|
179
|
-
|
|
192
|
+
Register it by hand if your agent needs it explicit:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{ "mcpServers": { "graft": { "command": "npx", "args": ["-y", "@nanonets/graft", "mcp"] } } }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Where a CLI agent supports user-level `hooks.json`, `init` also installs Graft's post-edit hook — blast-radius warnings and automatic `$0` graph re-sync after edits (skip with `--no-hooks`).
|
|
199
|
+
|
|
200
|
+
### Claude Code (deep integration)
|
|
201
|
+
|
|
202
|
+
`graft init` always wires up Claude Code, and Claude Code gets more than an instruction file. From then on, any Claude Code session opened in the repo gets:
|
|
203
|
+
|
|
204
|
+
- **a live statusline** — graph size, % enriched, and a `⚠ N stale` warning when the code has moved ahead of the graph
|
|
205
|
+
- **auto-sync** — after you edit code, Graft rebuilds the graph in the background at the end of the turn (structural, `$0` — it never calls the LLM on its own)
|
|
206
|
+
- **context on tap** — each prompt pulls the matching nodes into the session; editing a file surfaces what depends on it ("blast radius"); new sessions start with the repo map
|
|
207
|
+
|
|
208
|
+
<p align="center">
|
|
209
|
+
<img src="assets/graft-hook-blast-radius-demo.gif" alt="graft's post-edit hook: editing node-file.ts prints its blast radius (who depends on it) inline, the statusline flips stale → syncing → synced on its own, and the same dependents light up in graft viz" width="820"/>
|
|
210
|
+
<br/><sub>edit a file → blast radius appears inline → graph auto-resyncs → confirmed in <code>graft viz</code></sub>
|
|
211
|
+
</p>
|
|
212
|
+
|
|
213
|
+
`graft init` is idempotent and never clobbers your existing `.claude/settings.json` — it merges its blocks and leaves the rest alone. Want the LLM summaries too? Run `graft build --deep` (with a key) whenever you like; auto-sync will never do it for you.
|
|
180
214
|
|
|
181
215
|
---
|
|
182
216
|
|
|
@@ -191,6 +225,8 @@ graft ask "<task>" [dir] # query the graph — ranked nodes + exact
|
|
|
191
225
|
graft ask "<task>" --json # machine-readable result
|
|
192
226
|
graft ask "<task>" --in <scope> # narrow to one sub-project of a monorepo/multi-repo folder (see below)
|
|
193
227
|
|
|
228
|
+
graft skeleton <file> [dir] # every signature in one file, no bodies — the API surface for ~1/10th the tokens (no LLM, no key)
|
|
229
|
+
|
|
194
230
|
graft callers <symbol> [dir] # who calls/references/imports/implements/extends a symbol (no LLM, no key)
|
|
195
231
|
graft callers <symbol> --direction out # the reverse: what the symbol itself calls/references (was `graft callees`)
|
|
196
232
|
graft callers <symbol> -d N # walk transitively out to depth N — full blast radius (was `graft impact`)
|
|
@@ -249,15 +285,14 @@ with file/symbol counts, each dir's local hubs, and the global hotspots —
|
|
|
249
285
|
all ranked by in-degree, no LLM, no key:
|
|
250
286
|
|
|
251
287
|
```
|
|
252
|
-
repo map —
|
|
288
|
+
repo map — 113 files · 687 symbols · 2186 edges · typescript
|
|
253
289
|
|
|
254
|
-
src/
|
|
255
|
-
test/
|
|
290
|
+
src/ 63 files · 527 symbols hubs: contextDirFor (node-file.ts, 21←), wiringPath (write.ts, 14←), buildGraph (build.ts, 11←)
|
|
291
|
+
test/ 43 files · 102 symbols hubs: edge (graph-traverse.test.ts, 4←), graphOf (graph-traverse.test.ts, 4←), fileNode (graph-map.test.ts, 3←)
|
|
256
292
|
viewer/ 5 files · 58 symbols hubs: $ (main.ts, 9←), activeGraph (main.ts, 5←), cvar (data.ts, 5←)
|
|
257
|
-
bench/ 8 files · 42 symbols hubs: makeClient (llm.ts, 3←), buildMarkdown (report.ts, 2←), judge (judge.ts, 2←)
|
|
258
293
|
scripts/ 2 files · 0 symbols
|
|
259
294
|
|
|
260
|
-
hotspots:
|
|
295
|
+
hotspots: contextDirFor · function · src/context/node-file.ts:L100-L103 · 21← wiringPath · function · src/graph/write.ts:L20-L22 · 14← buildGraph · function · src/graph/build.ts:L104-L218 · 11← ...
|
|
261
296
|
```
|
|
262
297
|
|
|
263
298
|
## Monorepos & multi-repo folders
|
|
@@ -271,7 +306,7 @@ Graft handles two shapes without any config:
|
|
|
271
306
|
sub-project can't drown a small one; hits carry `[scope/]` labels, and
|
|
272
307
|
`graft map` groups its directory clusters by scope first.
|
|
273
308
|
- **A folder of separate git repos** (no `.git` at the top) — `graft build`
|
|
274
|
-
auto-splits: each child
|
|
309
|
+
auto-splits: each child gets its own (git-ignored) `graft/`, and the parent
|
|
275
310
|
gets a `graft/workspace.json` index. Queries from the parent federate across
|
|
276
311
|
every child, always labeled `<child>/`. Run `graft build` inside a child to
|
|
277
312
|
work on just that repo.
|
|
@@ -284,6 +319,11 @@ once you know where you're working.
|
|
|
284
319
|
`graft viz` opens a local, interactive view of both graphs — no install, no dev
|
|
285
320
|
server; the viewer ships prebuilt inside the package.
|
|
286
321
|
|
|
322
|
+
<p align="center">
|
|
323
|
+
<img src="assets/graft-viz-demo.gif" alt="graft viz — searching a symbol and jumping to it lights up its dependency graph: amber edges are what it depends on, teal is what depends on it" width="820"/>
|
|
324
|
+
<br/><sub>search → jump to a node → dependency graph lights up</sub>
|
|
325
|
+
</p>
|
|
326
|
+
|
|
287
327
|
- **Context** tab — the architecture graph from `graft/*.md`. Nodes colored by
|
|
288
328
|
type, sized by connectedness.
|
|
289
329
|
- **Code** tab — the per-symbol graph from `graft/.graph/wiring.json` (run `graft build` first).
|
|
@@ -312,26 +352,77 @@ normalized on load — no regeneration needed.
|
|
|
312
352
|
|
|
313
353
|
## Benchmark
|
|
314
354
|
|
|
315
|
-
|
|
355
|
+
An agent that reads the graph should be cheaper and faster without getting more answers wrong. That's the whole claim, so we measured it instead of asserting it.
|
|
316
356
|
|
|
317
|
-
|
|
357
|
+
The harness ran three variants of the same Claude Sonnet 5 agent with the same file tools: **cold** (explores from zero), **Graft** (a `graft ask --source` bundle pushed up front), and **pull** (graft_ask/graft_skeleton tools, nothing injected — context paid for only when asked). An Opus 4.8 judge scored correctness with a required-keyword floor, so a fast-but-wrong answer couldn't win by being fast. Cost is cache-aware: reads ≈0.1×, writes 1.25×, the billing model agents actually run under.
|
|
318
358
|
|
|
319
|
-
|
|
359
|
+
162 runs, two repos (graft itself and a real Node/Express auth service), 3 trials each, tasks split between single-file and multi-file questions.
|
|
360
|
+
|
|
361
|
+
| Metric (mean/task) | Cold | Graft |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| Cost ($) | 0.0429 | **0.0292 (−32%)** |
|
|
364
|
+
| Uncached input tokens | 8,070 | **4,650 (−42%)** |
|
|
365
|
+
| Tool calls | 4.2 | **2.3 (−46%)** |
|
|
366
|
+
| Latency (s) | 39.8 | **15.8 (−60%)** |
|
|
367
|
+
| Correctness | 93% | 93% (equal) |
|
|
368
|
+
|
|
369
|
+
Graft never answered worse than cold, on any corpus. The pull variant gave up most of that speed for something bigger: correctness jumped to 98%, +5 points over cold, the strongest single result in the sweep. Push when speed is what you need; pull when being right matters more.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Tested on your popular repos
|
|
374
|
+
|
|
375
|
+
The sweep above measures the mechanism. The real test is whether graft helps an agent **ship real changes** on code people actually run, not just answer questions. So we benchmark it on popular open-source repos: **15 tasks each**, 10 real developer questions plus **5 actual implementation tasks** (real merged pull requests, each re-implemented from its base commit and scored against the files the maintainers actually changed). Same agent (Claude Opus), same file tools; the only difference is whether graft is wired in.
|
|
376
|
+
|
|
377
|
+
Across these repos graft runs **up to 4× cheaper and 3× faster**, with better or no loss of correctness: it reproduces the real merged PRs by touching the same files the maintainers did. Per-repo detail below.
|
|
378
|
+
|
|
379
|
+
### PocketBase (Go, ~350 files)
|
|
380
|
+
|
|
381
|
+
| Aggregate over 15 tasks | Standard Claude Code | With graft |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| Cost | $13.91 | **$11.02 (−21%)** |
|
|
384
|
+
| Wall-clock | 2,044s | **1,762s (−14%)** |
|
|
385
|
+
| PRs reproduced | 5 / 5 | **5 / 5 (same files as the maintainers)** |
|
|
386
|
+
|
|
387
|
+
Cheaper and faster with no loss of correctness: graft reproduced all five merged PRs, touching the same files the maintainers did. The gap is widest on cross-file understanding — "how does auth work across OAuth2 providers" dropped from $2.19 to $0.84.
|
|
388
|
+
|
|
389
|
+
<details>
|
|
390
|
+
<summary><b>The 10 questions we asked</b></summary>
|
|
391
|
+
|
|
392
|
+
1. **Orientation** — Give me a map of PocketBase's architecture: the main subsystems and how an HTTP request flows through to the database.
|
|
393
|
+
2. **Entry-point trace** — Trace end-to-end what happens when a client creates a record via the REST API, from route handler to database write.
|
|
394
|
+
3. **Feature location** — I want to add a brand-new collection field type. Where do I hook it in, and which pieces must change?
|
|
395
|
+
4. **Bug localization** — Realtime subscriptions silently stop delivering events after a while. Where would you start looking, and why?
|
|
396
|
+
5. **Blast radius** — If I change the signature of the record-validation logic, what depends on it and what could break?
|
|
397
|
+
6. **Cross-file synthesis** — How does auth work across OAuth2 providers: where are tokens issued, validated, stored, and refreshed?
|
|
398
|
+
7. **Extensibility** — How do I use PocketBase as a Go framework to register a custom route plus an on-record-create hook?
|
|
399
|
+
8. **Security discovery** — Where is user input validated, and where are collection API access rules enforced before a query runs?
|
|
400
|
+
9. **Public API** — As an external app, how do I authenticate and then list and filter records over the REST API?
|
|
401
|
+
10. **Test verification** — Where are the tests for the record CRUD API, and what do they assert about access rules?
|
|
402
|
+
|
|
403
|
+
</details>
|
|
404
|
+
|
|
405
|
+
<details>
|
|
406
|
+
<summary><b>The 5 merged PRs we re-implemented</b></summary>
|
|
407
|
+
|
|
408
|
+
Each PR was reset to its base commit; graft's diff was scored against the files the merged PR changed.
|
|
409
|
+
|
|
410
|
+
| PR | Type | What it does | Files the maintainers touched |
|
|
320
411
|
|---|---|---|---|
|
|
321
|
-
|
|
|
322
|
-
|
|
|
323
|
-
|
|
|
324
|
-
|
|
|
325
|
-
|
|
|
412
|
+
| [#6744](https://github.com/pocketbase/pocketbase/pull/6744) | feat | Generate & serve WebP thumbnails | `apis/file.go`, `tools/filesystem/filesystem.go` |
|
|
413
|
+
| [#6947](https://github.com/pocketbase/pocketbase/pull/6947) | fix | Uniform char distribution in regex random strings | `tools/security/random_by_regex.go` |
|
|
414
|
+
| [#6690](https://github.com/pocketbase/pocketbase/pull/6690) | refactor | Patreon OAuth2 to use `x/oauth2/endpoints` | `tools/auth/patreon.go` |
|
|
415
|
+
| [#2726](https://github.com/pocketbase/pocketbase/pull/2726) | perf | Drop a redundant admin-count query on a hot middleware path | `apis/middlewares.go` |
|
|
416
|
+
| [#3192](https://github.com/pocketbase/pocketbase/pull/3192) | fix | Restore prior API rules on automigration rollback | `plugins/migratecmd/templates.go` |
|
|
326
417
|
|
|
327
|
-
|
|
418
|
+
</details>
|
|
328
419
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
420
|
+
<details>
|
|
421
|
+
<summary><b>Method</b></summary>
|
|
422
|
+
|
|
423
|
+
Two clones of PocketBase at the same commit: one wired with `graft init`, one untouched and verified graft-free. Each task run headless (`claude -p`, Claude Opus) with an empty MCP config. Understanding questions were graded by whether the answer pointed to the right files and functions; PR tasks were scored on whether the agent's diff touched the same files as the merged PR. Every transcript was audited to confirm graft was actually used in the graft arm and absent from the standard arm.
|
|
333
424
|
|
|
334
|
-
|
|
425
|
+
</details>
|
|
335
426
|
|
|
336
427
|
---
|
|
337
428
|
|
package/dist/ai/llm/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Provider-neutral chat transport.
|
|
3
3
|
*
|
|
4
4
|
* Every LLM call in graft — the engine's single-shot summarize/synthesize/crux
|
|
5
|
-
* ops
|
|
5
|
+
* ops, and any multi-turn tool-use loop built on top — goes through one interface,
|
|
6
6
|
* {@link ChatModel}. Adapters translate this neutral shape to a concrete SDK
|
|
7
7
|
* (OpenAI-compatible or native Anthropic). Nothing above this layer knows which
|
|
8
8
|
* provider is in play, so adding a provider is a new adapter, never a change to
|
|
@@ -37,7 +37,7 @@ export interface Message {
|
|
|
37
37
|
/**
|
|
38
38
|
* Request a provider cache marker (`cache_control: ephemeral`) on THIS message.
|
|
39
39
|
* The transport only understands this per-message flag; any sliding/rotation
|
|
40
|
-
* policy lives in the caller
|
|
40
|
+
* policy lives in the caller.
|
|
41
41
|
*/
|
|
42
42
|
cacheBreakpoint?: boolean;
|
|
43
43
|
/**
|
package/dist/ai/llm/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Provider-neutral chat transport.
|
|
3
3
|
*
|
|
4
4
|
* Every LLM call in graft — the engine's single-shot summarize/synthesize/crux
|
|
5
|
-
* ops
|
|
5
|
+
* ops, and any multi-turn tool-use loop built on top — goes through one interface,
|
|
6
6
|
* {@link ChatModel}. Adapters translate this neutral shape to a concrete SDK
|
|
7
7
|
* (OpenAI-compatible or native Anthropic). Nothing above this layer knows which
|
|
8
8
|
* provider is in play, so adding a provider is a new adapter, never a change to
|
package/dist/ask/ask.d.ts
CHANGED
|
@@ -58,6 +58,20 @@ export interface AskResult {
|
|
|
58
58
|
}[];
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
/** Score a document's token counts against the query counts (name field
|
|
62
|
+
* pre-weighted by caller). Each shared term is weighted by its inverse document
|
|
63
|
+
* frequency (`idf`), so a word that appears in many nodes — "overlay" across a
|
|
64
|
+
* whole widget subsystem, or a repeated word in a pasted issue body — counts for
|
|
65
|
+
* far less than a rare, discriminating identifier ("scrolling"). Without idf,
|
|
66
|
+
* pure term-frequency lets an incidental common word dominate the ranking; this
|
|
67
|
+
* is the lexical half of the keyword-collision fix (graph-rank is the other). */
|
|
68
|
+
/** Test files rarely answer "how does X work" / "where is Y" — they mirror the
|
|
69
|
+
* real symbol's tokens, so a test can out-score the definition on a lexical tie
|
|
70
|
+
* and land as the top hit (observed: an `ask` returning a `Test…` function first,
|
|
71
|
+
* sending the agent to the wrong file). A multiplicative de-rank keeps tests in
|
|
72
|
+
* the results (they still matter for "where are the tests") but below the real
|
|
73
|
+
* definition. Covers Go (`_test.go`), JS/TS (`.test.` / `.spec.`), and test dirs. */
|
|
74
|
+
export declare function isTestPath(path: string): boolean;
|
|
61
75
|
/** Idf-weighted share (0..1) of the query a document matches: each query term
|
|
62
76
|
* counts by its rarity, so this separates task prompts from chatter in a way a
|
|
63
77
|
* raw term count can't. A conversational prompt's words are either off-corpus
|
package/dist/ask/ask.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/ask/ask.ts"],"names":[],"mappings":"AAmBA,OAAO,
|
|
1
|
+
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/ask/ask.ts"],"names":[],"mappings":"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"}
|
package/dist/ask/ask.js
CHANGED
|
@@ -17,7 +17,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
17
17
|
import { join, resolve } from "node:path";
|
|
18
18
|
import matter from "gray-matter";
|
|
19
19
|
import { contextDirFor } from "../context/node-file.js";
|
|
20
|
-
import { savingsFooter, savingsFor } from "../context/savings.js";
|
|
20
|
+
import { savingsFooter, savingsFor, SAVINGS_TURN_NUDGE } from "../context/savings.js";
|
|
21
21
|
import { loadGraphCached, loadAskIndexCached } from "../graph/load.js";
|
|
22
22
|
import { pathUnderPrefix, scopeLabel, scopeOf, scopesHereClause, scopesOfGraph } from "../graph/scopes.js";
|
|
23
23
|
import { resolveSymbol } from "../graph/traverse.js";
|
|
@@ -71,6 +71,16 @@ function loadCorpus(outDir) {
|
|
|
71
71
|
* far less than a rare, discriminating identifier ("scrolling"). Without idf,
|
|
72
72
|
* pure term-frequency lets an incidental common word dominate the ranking; this
|
|
73
73
|
* is the lexical half of the keyword-collision fix (graph-rank is the other). */
|
|
74
|
+
/** Test files rarely answer "how does X work" / "where is Y" — they mirror the
|
|
75
|
+
* real symbol's tokens, so a test can out-score the definition on a lexical tie
|
|
76
|
+
* and land as the top hit (observed: an `ask` returning a `Test…` function first,
|
|
77
|
+
* sending the agent to the wrong file). A multiplicative de-rank keeps tests in
|
|
78
|
+
* the results (they still matter for "where are the tests") but below the real
|
|
79
|
+
* definition. Covers Go (`_test.go`), JS/TS (`.test.` / `.spec.`), and test dirs. */
|
|
80
|
+
export function isTestPath(path) {
|
|
81
|
+
return /(^|\/)(tests?|__tests__|spec)\/|(_test|\.test|\.spec)\.[a-z]+$/i.test(path || "");
|
|
82
|
+
}
|
|
83
|
+
const TEST_RANK_PENALTY = 0.35;
|
|
74
84
|
function score(query, doc, idf) {
|
|
75
85
|
let s = 0;
|
|
76
86
|
for (const [t, qn] of query) {
|
|
@@ -169,7 +179,7 @@ function findSubjectNodes(query, graph, inPrefix) {
|
|
|
169
179
|
}
|
|
170
180
|
function fallthroughNoteFor(subject) {
|
|
171
181
|
return (`structural index: no entries for '${subject}' — showing lexical matches; ` +
|
|
172
|
-
`for
|
|
182
|
+
`for precise edges try graft callers '${subject}', or graft grep '${subject}' for every reference (loosen the pattern if it returns nothing)`);
|
|
173
183
|
}
|
|
174
184
|
function structural(query, graph, limit, inPrefix) {
|
|
175
185
|
const wantsIn = INCOMING.test(query);
|
|
@@ -275,6 +285,13 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
275
285
|
// ranty description can't linearly amplify an incidental word.
|
|
276
286
|
const q = new Map([...counts(tokenize(query)).keys()].map((t) => [t, 1]));
|
|
277
287
|
const graph = corpus.graph;
|
|
288
|
+
// Test-file de-ranking is query-aware: a query that ASKS about tests wants test
|
|
289
|
+
// files on top, so it gets no penalty; every other query wants the real
|
|
290
|
+
// definition first. This stops a test that merely contains the query's literals
|
|
291
|
+
// (e.g. `TestDownloadStall` matching "download stall") from outranking the
|
|
292
|
+
// source it exercises — the "tests ranked above the actual code" trap.
|
|
293
|
+
const wantsTests = /\b(tests?|specs?|coverage|assert(?:ion)?s?|fixtures?|mocks?)\b/i.test(query);
|
|
294
|
+
const testFactor = (path) => (!wantsTests && isTestPath(path) ? TEST_RANK_PENALTY : 1);
|
|
278
295
|
// ── Pass 1: tokenize every scored field once, and collect per-document token
|
|
279
296
|
// bags so IDF can down-weight words that occur across the whole corpus. `--in`
|
|
280
297
|
// drops a concept unless at least one of its sources falls under the prefix —
|
|
@@ -429,9 +446,10 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
429
446
|
const avg = docs.reduce((a, d) => a + bodyLen(d.body), 0) / docs.length;
|
|
430
447
|
const out = new Map();
|
|
431
448
|
for (const { n, name, path, body } of docs) {
|
|
432
|
-
const total = score(q, name, sIdf) * 3 +
|
|
449
|
+
const total = (score(q, name, sIdf) * 3 +
|
|
433
450
|
score(q, path, sIdf) * 2 +
|
|
434
|
-
bm25(q, body, sIdf, bodyLen(body), avg)
|
|
451
|
+
bm25(q, body, sIdf, bodyLen(body), avg)) *
|
|
452
|
+
testFactor(n.path);
|
|
435
453
|
if (total > 0)
|
|
436
454
|
out.set(n.id, total);
|
|
437
455
|
}
|
|
@@ -501,9 +519,10 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
501
519
|
for (const { n, name, path, body } of symbolDocs) {
|
|
502
520
|
// Name and path are short identifiers → plain idf-weighted overlap; the body
|
|
503
521
|
// is length-normalized via BM25 so long definitions don't win on bulk.
|
|
504
|
-
const total = score(q, name, idf) * 3 +
|
|
522
|
+
const total = (score(q, name, idf) * 3 +
|
|
505
523
|
score(q, path, idf) * 2 +
|
|
506
|
-
bm25(q, body, idf, bodyLen(body), avgBodyLen)
|
|
524
|
+
bm25(q, body, idf, bodyLen(body), avgBodyLen)) *
|
|
525
|
+
testFactor(n.path);
|
|
507
526
|
if (total > 0) {
|
|
508
527
|
lex.set(n.id, total);
|
|
509
528
|
maxLex = Math.max(maxLex, total);
|
|
@@ -813,7 +832,8 @@ function askSavingsFooter(r, body) {
|
|
|
813
832
|
const pct = Math.round((saved / base) * 100);
|
|
814
833
|
return (`\n\n[graft] tokens saved ≈ ${saved.toLocaleString()} (${pct}%) — this pack ≈ ` +
|
|
815
834
|
`${pack.toLocaleString()} tok vs reading the ${r.saved.files} source file(s) whole ≈ ` +
|
|
816
|
-
`${base.toLocaleString()} tok. Estimate (baseline = those files read in full).`
|
|
835
|
+
`${base.toLocaleString()} tok. Estimate (baseline = those files read in full).` +
|
|
836
|
+
SAVINGS_TURN_NUDGE);
|
|
817
837
|
}
|
|
818
838
|
/** Multi-scope footer: answers a reviewer's two questions — "which
|
|
819
839
|
* sub-projects did this pack come from?" (`matched in:`, displayed-hit counts,
|