whygit 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +267 -2
- package/README.zh-CN.md +264 -0
- package/bin/whygit.js +11 -0
- package/docs/demo.svg +21 -0
- package/package.json +50 -4
- package/src/cli.js +132 -0
- package/src/cmd-check.js +81 -0
- package/src/cmd-cursed.js +38 -0
- package/src/cmd-init.js +92 -0
- package/src/cmd-story.js +85 -0
- package/src/cmd-why.js +105 -0
- package/src/context.js +63 -0
- package/src/db.js +76 -0
- package/src/git.js +311 -0
- package/src/indexer.js +150 -0
- package/src/install.js +95 -0
- package/src/mcp.js +171 -0
- package/src/risk.js +44 -0
- package/src/search.js +161 -0
- package/src/signals.js +160 -0
- package/src/ui.js +103 -0
- package/src/util.js +200 -0
- package/src/whylog.js +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 whygit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,268 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# whygit
|
|
4
|
+
|
|
5
|
+
**Git blame tells you *who*. whygit tells you *why*.**
|
|
6
|
+
|
|
7
|
+
Point it at any repository. It turns the reasons already buried in git history
|
|
8
|
+
into answers you and your coding agent can cite 鈥?file, line, commit, author, date.
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/whygit)
|
|
11
|
+
[](https://github.com/carefreebit/whygit/actions/workflows/ci.yml)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](#give-it-to-your-agent)
|
|
15
|
+
|
|
16
|
+
[涓枃](README.zh-CN.md) 路 [Quickstart](#quickstart) 路 [Commands](#commands) 路 [Agents](#give-it-to-your-agent) 路 [How it works](#how-it-works)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="https://raw.githubusercontent.com/carefreebit/whygit/main/docs/demo.svg" alt="whygit why "cookie jar" in a terminal" width="900">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><sub>Real output: <code>whygit why "cookie jar"</code> on <code>psf/requests</code> 鈥?4,884 commits indexed in 3s, answered in 21ms.</sub></p>
|
|
27
|
+
|
|
28
|
+
That answer existed all along 鈥?in a commit body from 2015 that nobody has read since.
|
|
29
|
+
whygit found it in 21 milliseconds, without an LLM, an API key, or a network call.
|
|
30
|
+
|
|
31
|
+
## The problem
|
|
32
|
+
|
|
33
|
+
Every codebase is full of lines that look wrong but are not. A retry that seems
|
|
34
|
+
redundant. A guard that seems paranoid. A `sleep(2)` that looks like laziness.
|
|
35
|
+
|
|
36
|
+
- `git blame` tells you **who** touched the line and **when**. Not why.
|
|
37
|
+
- `git log` is a write-only database. The reason is in the body of a commit from 2016.
|
|
38
|
+
- LLM summaries of history **invent plausible reasons**. In a codebase, a plausible
|
|
39
|
+
wrong reason is worse than no reason.
|
|
40
|
+
- Coding agents are the worst affected: they rewrite the guard, delete the retry, and
|
|
41
|
+
the bug comes back six months later 鈥?because nothing told them the line was load-bearing.
|
|
42
|
+
|
|
43
|
+
whygit reads what your repository already wrote down, extracts the sentences that
|
|
44
|
+
actually explain a decision, and makes them retrievable in the two places that matter:
|
|
45
|
+
**your terminal** and **your agent's context window**.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx whygit init # one pass over history, ~2s for 5k commits
|
|
51
|
+
npx whygit why "retry on 503" # natural-language question
|
|
52
|
+
npx whygit why src/client.js:42 # what is this exact line doing here?
|
|
53
|
+
npx whygit cursed # the files that keep coming back
|
|
54
|
+
npx whygit story src/client.js # one file, birth to HEAD
|
|
55
|
+
npx whygit whylog --write # write WHYLOG.md, the file agents read
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires **Node 22.5+** (for the built-in `node:sqlite`). Zero runtime dependencies.
|
|
59
|
+
No config file, no account, no telemetry.
|
|
60
|
+
|
|
61
|
+
The first command builds a local index at `.git/whygit/index.db`. Every command after
|
|
62
|
+
it is incremental: new commits are picked up automatically on the next run.
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
| Command | What it answers |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `whygit why <words>` | Why does this behaviour exist? Ranked decisions with citations. |
|
|
69
|
+
| `whygit why <file>:<line>` | Why is *this line* still here? Blame + the decision behind it + how much of that change is still alive. |
|
|
70
|
+
| `whygit story <file>` | The biography of one file: birth, commits per year, the chapters that mattered, who kept it alive. |
|
|
71
|
+
| `whygit cursed` | Ranked risk map: churn 脳 fix density 脳 author spread 脳 freshness. Read before refactoring. |
|
|
72
|
+
| `whygit whylog --write` | Generate `WHYLOG.md`: load-bearing decisions + cursed files, in Markdown. |
|
|
73
|
+
| `whygit check --base origin/main` | CI gate: what fraction of newly added lines trace to a commit that said why. |
|
|
74
|
+
| `whygit mcp` | MCP server over stdio for Claude Code, Cursor, Codex, and any MCP client. |
|
|
75
|
+
| `whygit install <agent>` | Writes the MCP config (`claude`, `cursor`, `vscode`, `agents`, `all`). |
|
|
76
|
+
| `whygit doctor` | Checks git, index freshness, shallow clones, missing WHYLOG. |
|
|
77
|
+
|
|
78
|
+
Every command takes `--json` for scripting, and `--cwd <path>` to point at another repo.
|
|
79
|
+
|
|
80
|
+
### `why <file>:<line>`
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
$ whygit why src/requests/models.py:120
|
|
84
|
+
|
|
85
|
+
1. basics working
|
|
86
|
+
2012-12-15 路 366e8e84 路 Kenneth Reitz 路 change
|
|
87
|
+
line 120 is 78 lines of this change, still in HEAD
|
|
88
|
+
|
|
89
|
+
2. Add hasattr checks for remaining protocol isinstance checks (#7505)
|
|
90
|
+
2026-06-09 路 6f66281a 路 change
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The line itself is unremarkable. What matters is that 78 lines around it are still
|
|
94
|
+
the original 2012 change 鈥?so the surrounding shape is load-bearing, not incidental.
|
|
95
|
+
|
|
96
|
+
### `story <file>`
|
|
97
|
+
|
|
98
|
+
```console
|
|
99
|
+
$ whygit story src/requests/models.py
|
|
100
|
+
|
|
101
|
+
born 2011-05-14 路 package refactor
|
|
102
|
+
|
|
103
|
+
2011 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅路路路路路路 217
|
|
104
|
+
2012 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 254
|
|
105
|
+
2013 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅路路路路路路路路路路路路路路路路路路路路路路路路路路 86
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
717 commits 路 202 authors 路 154 fixes 路 0 TODO markers
|
|
109
|
+
鈹€鈹€鈹€ the plot 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
110
|
+
|
|
111
|
+
2015-04-06 Copy a PreparedRequest's CookieJar reliably
|
|
112
|
+
By adding _copy_cookie_jar we can reliably copy cookie jars so that we
|
|
113
|
+
have an actual copy instead of the same instance on different prepared
|
|
114
|
+
requests.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `cursed`
|
|
118
|
+
|
|
119
|
+
```console
|
|
120
|
+
$ whygit cursed
|
|
121
|
+
|
|
122
|
+
1 src/requests/models.py 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 8.1 717 commits 路 202 authors 路 154 fixes
|
|
123
|
+
2 src/requests/sessions.py 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻埪? 7.5 334 commits 路 110 authors 路 79 fixes
|
|
124
|
+
3 tests/test_requests.py 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅路路 7.2 366 commits 路 104 authors 路 76 fixes
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Not a code-quality judgement 鈥?a map of where history is dense enough that a
|
|
128
|
+
confident rewrite is likely to be wrong.
|
|
129
|
+
|
|
130
|
+
## Give it to your agent
|
|
131
|
+
|
|
132
|
+
An agent without history rewrites load-bearing code. An agent with history asks
|
|
133
|
+
better questions. One command wires it up:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx whygit install claude # .mcp.json
|
|
137
|
+
npx whygit install cursor # .cursor/mcp.json
|
|
138
|
+
npx whygit install vscode # .vscode/mcp.json
|
|
139
|
+
npx whygit install agents # appends a block to AGENTS.md (works with any agent)
|
|
140
|
+
npx whygit install all
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Add `--local` if `whygit` is on your `PATH` (otherwise the config uses `npx -y whygit@latest`).
|
|
144
|
+
|
|
145
|
+
The MCP server exposes five tools:
|
|
146
|
+
|
|
147
|
+
| Tool | Use it when |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `why_search` | The agent is about to change behaviour it does not understand. |
|
|
150
|
+
| `why_line` | The agent is editing a specific line and needs the decision behind it. |
|
|
151
|
+
| `why_file` | The agent is refactoring a file and needs every decision that shaped it. |
|
|
152
|
+
| `why_risk` | The agent is choosing where to start, or where *not* to. |
|
|
153
|
+
| `whylog_read` | The agent wants the whole distilled history in one read. |
|
|
154
|
+
|
|
155
|
+
Add one line to your agent instructions, and the behaviour changes:
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
Before changing a line you do not understand, call `why_line` (MCP: whygit).
|
|
159
|
+
If a past decision contradicts your plan, say so before editing.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### WHYLOG.md 鈥?history your agent reads without being asked
|
|
163
|
+
|
|
164
|
+
`whygit whylog --write` produces a Markdown file that is small enough to live in a
|
|
165
|
+
repo and specific enough to change a decision:
|
|
166
|
+
|
|
167
|
+
- **Load-bearing decisions** 鈥?ranked, each with the verbatim reason, date, commit, author, and the files it touches.
|
|
168
|
+
- **The cursed files** 鈥?a table of where history is thickest.
|
|
169
|
+
- **House rule** 鈥?the one-line convention (`WHY: ...`) that makes future commits explain themselves.
|
|
170
|
+
|
|
171
|
+
It is generated from git, never hand-edited, and it diffs cleanly in review.
|
|
172
|
+
Commit it, and every agent that starts in the repo gets the context for free 鈥?no MCP required.
|
|
173
|
+
|
|
174
|
+
### CI gate
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- run: npx whygit check --base origin/main --min 0.6
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Fails when fewer than 60% of newly added lines trace to a commit that stated a reason.
|
|
181
|
+
It is a nudge, not a linter: the fix is one sentence in the commit body.
|
|
182
|
+
|
|
183
|
+
## How it works
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
git log (two passes: metadata + numstat)
|
|
187
|
+
鈹? 鈻? signals: strip trailers, drop bot noise, de-duplicate squash-merge subjects,
|
|
188
|
+
keep sentences that contain a reason ("because", "so that", "to avoid",
|
|
189
|
+
"鐢变簬", "閬垮厤", "鍥炴粴" ...)
|
|
190
|
+
鈹? 鈻? SQLite .git/whygit/index.db commits 路 files 路 decisions 路 FTS5 路 risk
|
|
191
|
+
鈹? 鈻? ranking: BM25 脳 confidence 脳 churn 脳 recency 脳 fix-density 脳 noise penalties
|
|
192
|
+
鈹? 鈹溾攢鈹€ whygit why / story / cursed (terminal)
|
|
193
|
+
鈹溾攢鈹€ whygit whylog (WHYLOG.md)
|
|
194
|
+
鈹斺攢鈹€ whygit mcp (agents)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**What a "decision" is:** a commit whose message contains a sentence explaining a
|
|
198
|
+
choice. whygit does not interpret that sentence 鈥?it quotes it. If the message says
|
|
199
|
+
nothing, the decision has low confidence and ranks below the ones that do.
|
|
200
|
+
|
|
201
|
+
**Why it can be fast:** the whole index is built from two `git log` passes, and
|
|
202
|
+
queries run against FTS5 in the same process. On a laptop, 4,884 commits index in
|
|
203
|
+
~3 seconds and a query takes ~20 ms. No network, no model, no cache to warm.
|
|
204
|
+
|
|
205
|
+
**What it will not do:** invent a reason, summarise code, or judge quality. If the
|
|
206
|
+
history is silent, whygit says so 鈥?silence is a finding too.
|
|
207
|
+
|
|
208
|
+
## Compared to
|
|
209
|
+
|
|
210
|
+
| | Answers "why" | Cites the source | Works offline | Agent-native |
|
|
211
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
212
|
+
| `git blame` | no | 鈥?| yes | no |
|
|
213
|
+
| `git log` | only if you already know where to look | yes | yes | no |
|
|
214
|
+
| LLM-generated docs | sometimes | rarely | no | partly |
|
|
215
|
+
| **whygit** | **yes** | **yes (sha + file + line)** | **yes** | **yes (MCP + WHYLOG.md)** |
|
|
216
|
+
|
|
217
|
+
## FAQ
|
|
218
|
+
|
|
219
|
+
**Does it send my code anywhere?** No. whygit reads local git data and writes one
|
|
220
|
+
SQLite file inside `.git/`. There is no network code path.
|
|
221
|
+
|
|
222
|
+
**Does it need an LLM?** No. That is the point: a cited sentence from 2016 beats a
|
|
223
|
+
confident paraphrase from a model.
|
|
224
|
+
|
|
225
|
+
**Does it work on a shallow clone?** It runs, and it tells you the history is
|
|
226
|
+
truncated (`whygit doctor`). Results are only as good as the history present.
|
|
227
|
+
|
|
228
|
+
**Squash merges?** Handled: the repeated subject line is removed and the PR body is
|
|
229
|
+
treated as the rationale.
|
|
230
|
+
|
|
231
|
+
**Monorepos?** Yes. Paths are indexed as-is; `why <file>:<line>` works from any depth.
|
|
232
|
+
|
|
233
|
+
**Does it modify my repository?** Only if you ask: `whylog --write`, `install`, and the
|
|
234
|
+
index under `.git/`. History is read-only.
|
|
235
|
+
|
|
236
|
+
## Limitations
|
|
237
|
+
|
|
238
|
+
- Rationale can only be as good as commit messages. whygit makes silence visible; it cannot fill it.
|
|
239
|
+
- Very large histories (500k+ commits) index in minutes, not seconds.
|
|
240
|
+
- Renames are followed by path, not by content similarity: a file moved twice may have two histories.
|
|
241
|
+
- The ranking is heuristic. It is tuned to prefer cited reasons over churn, and it will still be wrong sometimes 鈥?each result shows its citations so you can check.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git clone https://github.com/carefreebit/whygit && cd whygit
|
|
247
|
+
npm test # 17 tests, no network
|
|
248
|
+
npm run lint
|
|
249
|
+
node bin/whygit.js why "..." --cwd /path/to/any/repo
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Issues and PRs welcome 鈥?especially real-world ranking failures. Paste the query, the
|
|
253
|
+
repo, and what should have ranked first.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT. See [LICENSE](LICENSE).
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
<div align="center">
|
|
262
|
+
|
|
263
|
+
If whygit saved you from deleting a load-bearing line, a star helps the next person
|
|
264
|
+
find it.
|
|
265
|
+
|
|
266
|
+
**[猸?Star whygit](https://github.com/carefreebit/whygit)**
|
|
267
|
+
|
|
268
|
+
</div>
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# whygit
|
|
4
|
+
|
|
5
|
+
**git blame 告诉你「谁」改的。whygit 告诉你「为什么」。**
|
|
6
|
+
|
|
7
|
+
对着任意仓库跑一次,它把散落在 git 历史里的理由变成可引用的答案:
|
|
8
|
+
文件、行号、commit、作者、日期,一个都不少。
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/whygit)
|
|
11
|
+
[](https://github.com/carefreebit/whygit/actions/workflows/ci.yml)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](#交给你的-agent)
|
|
15
|
+
|
|
16
|
+
[English](README.md) · [快速开始](#快速开始) · [命令](#命令) · [Agent 接入](#交给你的-agent) · [原理](#它是怎么做到的)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="https://raw.githubusercontent.com/carefreebit/whygit/main/docs/demo.svg" alt="whygit why "cookie jar" 终端演示" width="900">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><sub>真实输出:在 <code>psf/requests</code> 上执行 <code>whygit why "cookie jar"</code> —— 4,884 个 commit 索引 3 秒,回答 21 毫秒。</sub></p>
|
|
27
|
+
|
|
28
|
+
这个答案一直躺在仓库里——2015 年某次 commit 的正文,此后九年没人读过。
|
|
29
|
+
whygit 用 21 毫秒把它找出来:没有大模型,没有 API key,没有网络请求。
|
|
30
|
+
|
|
31
|
+
## 它解决什么问题
|
|
32
|
+
|
|
33
|
+
每个代码库里都有一批「看起来很蠢、其实很关键」的代码:
|
|
34
|
+
一次显得多余的重试、一个看起来神经质的判空、一句像偷懒的 `sleep(2)`。
|
|
35
|
+
|
|
36
|
+
- `git blame` 只回答**谁**、**什么时候**动了这一行,不回答为什么;
|
|
37
|
+
- `git log` 是个只写不读的数据库,理由埋在 2016 年某条 commit 正文里;
|
|
38
|
+
- 让大模型去总结 git 历史,它会**编出看起来合理的理由**。在代码库里,一个听起来对的错理由,比没有理由更危险;
|
|
39
|
+
- 编码 Agent 受害最深:它们删掉重试、改写判空,半年后同一个 bug 回来——因为没有任何东西告诉它「这行是承重的」。
|
|
40
|
+
|
|
41
|
+
whygit 只做一件事:把仓库**已经写下的**理由读出来,提取真正在解释决策的句子,
|
|
42
|
+
送到两个真正有用的地方——**你的终端**,和 **Agent 的上下文窗口**。
|
|
43
|
+
|
|
44
|
+
## 快速开始
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx whygit init # 扫一遍历史,5k commit 约 2 秒
|
|
48
|
+
npx whygit why "retry on 503" # 用自然语言问
|
|
49
|
+
npx whygit why src/client.js:42 # 这一行到底为什么在这里
|
|
50
|
+
npx whygit cursed # 反复出问题的文件排行
|
|
51
|
+
npx whygit story src/client.js # 单个文件从诞生到 HEAD 的一生
|
|
52
|
+
npx whygit whylog --write # 生成 WHYLOG.md,给 Agent 读的历史笔记
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
需要 **Node 22.5+**(用到内置的 `node:sqlite`)。零运行时依赖,无配置文件,
|
|
56
|
+
无账号,无遥测。
|
|
57
|
+
|
|
58
|
+
第一次运行会在 `.git/whygit/index.db` 建本地索引;之后每次都是增量更新,
|
|
59
|
+
新 commit 会被自动补进来。
|
|
60
|
+
|
|
61
|
+
## 命令
|
|
62
|
+
|
|
63
|
+
| 命令 | 回答什么 |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `whygit why <关键词>` | 这个行为为什么存在?按相关度排序,每条都带出处。 |
|
|
66
|
+
| `whygit why <文件>:<行号>` | 这一行为什么还活着?blame + 背后的决策 + 这次改动还剩多少行在 HEAD。 |
|
|
67
|
+
| `whygit story <文件>` | 一个文件的传记:出生、按年提交量、关键章节、谁一直在维护它。 |
|
|
68
|
+
| `whygit cursed` | 风险地图:改动量 × 修复密度 × 作者分散度 × 新鲜度。重构前先看。 |
|
|
69
|
+
| `whygit whylog --write` | 生成 `WHYLOG.md`:承重决策 + 高危文件,纯 Markdown。 |
|
|
70
|
+
| `whygit check --base origin/main` | CI 门禁:新增代码行里,有多大比例能追溯到「说过为什么」的 commit。 |
|
|
71
|
+
| `whygit mcp` | 基于 stdio 的 MCP 服务,Claude Code、Cursor、Codex 及任何 MCP 客户端可用。 |
|
|
72
|
+
| `whygit install <agent>` | 写入 MCP 配置(`claude` / `cursor` / `vscode` / `agents` / `all`)。 |
|
|
73
|
+
| `whygit doctor` | 自检:git、索引是否过期、是否浅克隆、WHYLOG 是否缺失。 |
|
|
74
|
+
|
|
75
|
+
所有命令都支持 `--json`(给脚本用)和 `--cwd <path>`(指向别的仓库)。
|
|
76
|
+
|
|
77
|
+
### `why <文件>:<行号>`
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ whygit why src/requests/models.py:120
|
|
81
|
+
|
|
82
|
+
1. basics working
|
|
83
|
+
2012-12-15 · 366e8e84 · Kenneth Reitz · change
|
|
84
|
+
line 120 is 78 lines of this change, still in HEAD
|
|
85
|
+
|
|
86
|
+
2. Add hasattr checks for remaining protocol isinstance checks (#7505)
|
|
87
|
+
2026-06-09 · 6f66281a · change
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
这一行本身平平无奇。真正重要的是:它周围 78 行仍然来自 2012 年那次改动——
|
|
91
|
+
也就是说,这里的结构是承重的,不是随手写的。
|
|
92
|
+
|
|
93
|
+
### `story <文件>`
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ whygit story src/requests/models.py
|
|
97
|
+
|
|
98
|
+
born 2011-05-14 · package refactor
|
|
99
|
+
|
|
100
|
+
2011 ██████████████████████████████████······ 217
|
|
101
|
+
2012 ████████████████████████████████████████ 254
|
|
102
|
+
2013 ██████████████·························· 86
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
717 commits · 202 authors · 154 fixes · 0 TODO markers
|
|
106
|
+
─── the plot ──────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
2015-04-06 Copy a PreparedRequest's CookieJar reliably
|
|
109
|
+
By adding _copy_cookie_jar we can reliably copy cookie jars so that we
|
|
110
|
+
have an actual copy instead of the same instance on different prepared
|
|
111
|
+
requests.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `cursed`
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
$ whygit cursed
|
|
118
|
+
|
|
119
|
+
1 src/requests/models.py ██████████████ 8.1 717 commits · 202 authors · 154 fixes
|
|
120
|
+
2 src/requests/sessions.py █████████████· 7.5 334 commits · 110 authors · 79 fixes
|
|
121
|
+
3 tests/test_requests.py ████████████·· 7.2 366 commits · 104 authors · 76 fixes
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
这不是代码质量评分,而是一张地图:标出历史密度最高的地方——在那里自信地重写,
|
|
125
|
+
大概率是错的。
|
|
126
|
+
|
|
127
|
+
## 交给你的 Agent
|
|
128
|
+
|
|
129
|
+
不知道历史的 Agent 会重写承重代码;知道历史的 Agent 会先问对问题。一条命令接好:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx whygit install claude # .mcp.json
|
|
133
|
+
npx whygit install cursor # .cursor/mcp.json
|
|
134
|
+
npx whygit install vscode # .vscode/mcp.json
|
|
135
|
+
npx whygit install agents # 往 AGENTS.md 追加一段(任何 Agent 都认)
|
|
136
|
+
npx whygit install all
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
如果 `whygit` 已在 `PATH` 里,加 `--local`(否则配置里用 `npx -y whygit@latest`)。
|
|
140
|
+
|
|
141
|
+
MCP 服务暴露五个工具:
|
|
142
|
+
|
|
143
|
+
| 工具 | 什么时候用 |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `why_search` | Agent 即将改动一段它并不理解的行为。 |
|
|
146
|
+
| `why_line` | Agent 正在编辑某一行,需要这行背后的决策。 |
|
|
147
|
+
| `why_file` | Agent 要重构一个文件,需要塑造过它的全部决策。 |
|
|
148
|
+
| `why_risk` | Agent 在挑起点,或者在挑「别碰哪里」。 |
|
|
149
|
+
| `whylog_read` | Agent 想一口气读完蒸馏后的历史。 |
|
|
150
|
+
|
|
151
|
+
在 Agent 的指令文件里加一行,行为就变了:
|
|
152
|
+
|
|
153
|
+
```markdown
|
|
154
|
+
改动任何你不理解的行之前,先调用 `why_line`(MCP:whygit)。
|
|
155
|
+
如果历史决策和你的方案冲突,先说出来,再动手。
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### WHYLOG.md —— Agent 不用被提醒就会读的历史
|
|
159
|
+
|
|
160
|
+
`whygit whylog --write` 生成一份 Markdown:小到可以进仓库,具体到能改变一次决策。
|
|
161
|
+
|
|
162
|
+
- **承重决策**:按重要性排序,每条都带原文理由、日期、commit、作者、涉及文件;
|
|
163
|
+
- **高危文件**:历史最稠密的地方;
|
|
164
|
+
- **仓库规矩**:一行约定(`WHY: ...`),让以后的 commit 自己解释自己。
|
|
165
|
+
|
|
166
|
+
它由 git 生成、永不手改、在 review 里 diff 干净。提交进仓库后,
|
|
167
|
+
每个在这个仓库里启动的 Agent 都免费获得上下文——**不需要 MCP**。
|
|
168
|
+
|
|
169
|
+
### CI 门禁
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
- run: npx whygit check --base origin/main --min 0.6
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
当新增代码行中能追溯到「说过理由」的 commit 的比例低于 60% 时失败。
|
|
176
|
+
它不是 linter,只是提醒:修复方式是在 commit 正文里写一句话。
|
|
177
|
+
|
|
178
|
+
## 它是怎么做到的
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
git log(两遍:元数据 + numstat)
|
|
182
|
+
│
|
|
183
|
+
▼
|
|
184
|
+
信号提取:去掉 trailer、过滤机器人噪音、去重 squash-merge 重复标题,
|
|
185
|
+
保留含因果词的句子(because / so that / to avoid / 由于 / 避免 / 回滚 ...)
|
|
186
|
+
│
|
|
187
|
+
▼
|
|
188
|
+
SQLite .git/whygit/index.db commits · files · decisions · FTS5 · risk
|
|
189
|
+
│
|
|
190
|
+
▼
|
|
191
|
+
排序:BM25 × 置信度 × 改动量 × 新鲜度 × 修复密度 × 噪音惩罚
|
|
192
|
+
│
|
|
193
|
+
├── whygit why / story / cursed (终端)
|
|
194
|
+
├── whygit whylog (WHYLOG.md)
|
|
195
|
+
└── whygit mcp (Agent)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**什么叫一条「决策」**:commit message 里存在解释选择的句子。
|
|
199
|
+
whygit 不解释这句话,只引用它。如果 message 什么都没说,这条决策置信度就低,
|
|
200
|
+
排在说过理由的后面。
|
|
201
|
+
|
|
202
|
+
**为什么快**:整个索引只来自两遍 `git log`,查询在同一个进程里打 FTS5。
|
|
203
|
+
在普通笔记本上,4,884 个 commit 建索引约 3 秒,单次查询约 20 毫秒。
|
|
204
|
+
没有网络、没有模型、没有缓存要预热。
|
|
205
|
+
|
|
206
|
+
**它不会做什么**:编理由、总结代码、评判质量。历史沉默时,whygit 就说沉默——
|
|
207
|
+
沉默本身也是一种结论。
|
|
208
|
+
|
|
209
|
+
## 和其他方案比
|
|
210
|
+
|
|
211
|
+
| | 回答「为什么」 | 给出处 | 可离线 | 为 Agent 而生 |
|
|
212
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
213
|
+
| `git blame` | 否 | — | 是 | 否 |
|
|
214
|
+
| `git log` | 只有你已经知道去哪找时 | 是 | 是 | 否 |
|
|
215
|
+
| 大模型生成的文档 | 有时 | 很少 | 否 | 部分 |
|
|
216
|
+
| **whygit** | **是** | **是(sha + 文件 + 行)** | **是** | **是(MCP + WHYLOG.md)** |
|
|
217
|
+
|
|
218
|
+
## 常见问题
|
|
219
|
+
|
|
220
|
+
**会把我的代码传到网上吗?** 不会。whygit 只读本地 git 数据,只往 `.git/` 里写一个 SQLite 文件,
|
|
221
|
+
代码里没有任何网络调用路径。
|
|
222
|
+
|
|
223
|
+
**需要大模型吗?** 不需要,这正是重点:一句 2016 年带出处的原话,胜过模型的自信转述。
|
|
224
|
+
|
|
225
|
+
**浅克隆能用吗?** 能跑,但会明确告诉你历史被截断了(`whygit doctor`)。结果只和现存历史一样好。
|
|
226
|
+
|
|
227
|
+
**squash merge 呢?** 已处理:去掉正文里重复的标题行,把 PR 描述当作理由。
|
|
228
|
+
|
|
229
|
+
**monorepo?** 支持。路径按原样索引,`why <文件>:<行号>` 在任意层级都可用。
|
|
230
|
+
|
|
231
|
+
**会改我的仓库吗?** 只在你明确要求时:`whylog --write`、`install`,以及 `.git/` 下的索引。
|
|
232
|
+
历史始终只读。
|
|
233
|
+
|
|
234
|
+
## 局限
|
|
235
|
+
|
|
236
|
+
- 理由的上限就是 commit message 的质量。whygit 让沉默可见,但没法替你补上;
|
|
237
|
+
- 超大历史(50 万+ commit)建索引是分钟级,不是秒级;
|
|
238
|
+
- 重命名按路径追踪,不做内容相似度匹配:一个文件搬过两次,可能有两段历史;
|
|
239
|
+
- 排序是启发式的,偏向「有出处的理由」而不是改动量,仍会出错——每条结果都带引用,方便你自己核对。
|
|
240
|
+
|
|
241
|
+
## 参与贡献
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
git clone https://github.com/carefreebit/whygit && cd whygit
|
|
245
|
+
npm test # 17 个测试,不联网
|
|
246
|
+
npm run lint
|
|
247
|
+
node bin/whygit.js why "..." --cwd /path/to/any/repo
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
欢迎 issue 和 PR——尤其是真实的排序失败案例。请附上查询、仓库,以及你认为本应排第一的结果。
|
|
251
|
+
|
|
252
|
+
## 许可证
|
|
253
|
+
|
|
254
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
<div align="center">
|
|
259
|
+
|
|
260
|
+
如果 whygit 帮你拦下了一次「删掉承重代码」的操作,点个 star,能让下一个人找到它。
|
|
261
|
+
|
|
262
|
+
**[⭐ Star whygit](https://github.com/carefreebit/whygit)**
|
|
263
|
+
|
|
264
|
+
</div>
|
package/bin/whygit.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { main } from '../src/cli.js';
|
|
4
|
+
|
|
5
|
+
main(process.argv.slice(2))
|
|
6
|
+
.then(function (code) { process.exitCode = code || 0; })
|
|
7
|
+
.catch(function (err) {
|
|
8
|
+
process.stderr.write('whygit: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
9
|
+
if (process.env.WHYGIT_DEBUG) console.error(err);
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
});
|
package/docs/demo.svg
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="570" viewBox="0 0 900 570" role="img" aria-label="whygit terminal demo">
|
|
2
|
+
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#161b22"/><stop offset="1" stop-color="#0d1117"/></linearGradient></defs>
|
|
3
|
+
<rect width="900" height="570" rx="14" fill="url(#bg)" stroke="#30363d"/>
|
|
4
|
+
<rect width="900" height="42" rx="14" fill="#21262d"/><rect y="28" width="900" height="14" fill="#21262d"/>
|
|
5
|
+
<circle cx="24" cy="21" r="6" fill="#ff5f56"/><circle cx="46" cy="21" r="6" fill="#ffbd2e"/><circle cx="68" cy="21" r="6" fill="#27c93f"/>
|
|
6
|
+
<text x="450" y="26" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="12" fill="#8b949e">whygit — psf/requests</text>
|
|
7
|
+
<text x="30" y="74" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#7ee787" font-weight="600" xml:space="preserve">$ whygit why "cookie jar"</text>
|
|
8
|
+
<text x="30" y="116" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#e6edf3" font-weight="600" xml:space="preserve"> whygit psf/requests 4,884 commits</text>
|
|
9
|
+
<text x="30" y="137" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#30363d" xml:space="preserve"> ────────────────────────────────────</text>
|
|
10
|
+
<text x="30" y="200" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#f0f6fc" font-weight="600" xml:space="preserve"> 1. Copy a PreparedRequest's CookieJar reliably</text>
|
|
11
|
+
<text x="30" y="221" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2015-04-06 · 36093e69 · Ian Cordasco · change · 100% match</text>
|
|
12
|
+
<text x="30" y="263" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> By adding _copy_cookie_jar we can reliably copy cookie jars so that we have an</text>
|
|
13
|
+
<text x="30" y="284" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> actual copy instead of the same instance on different prepared requests.</text>
|
|
14
|
+
<text x="30" y="326" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#79c0ff" xml:space="preserve"> requests/cookies.py requests/models.py</text>
|
|
15
|
+
<text x="30" y="347" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#58a6ff" text-decoration="underline" xml:space="preserve"> → https://github.com/psf/requests/commit/36093e69c70b9b24dd1e007befa35ad891313f51</text>
|
|
16
|
+
<text x="30" y="389" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#f0f6fc" font-weight="600" xml:space="preserve"> 2. Test and perfection for cookie handling.</text>
|
|
17
|
+
<text x="30" y="410" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2013-02-11 · 2e316961 · Ian Cordasco · fix · 71% match</text>
|
|
18
|
+
<text x="30" y="452" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> I also fixed up some of the RequestsCookieJar methods so using</text>
|
|
19
|
+
<text x="30" y="473" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#c9d1d9" xml:space="preserve"> jar.update(other_jar) works without a problem.</text>
|
|
20
|
+
<text x="30" y="515" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13.5" fill="#8b949e" xml:space="preserve"> 2 of 15 decisions · 21ms · whygit why "cookie jar"</text>
|
|
21
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whygit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Git blame tells you who. whygit tells you why. Turn git history into cited, agent-readable context.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"whygit": "bin/whygit.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22.5.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"docs",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.zh-CN.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test test/signals.test.js test/index.test.js",
|
|
22
|
+
"lint": "node scripts/lint.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"git",
|
|
26
|
+
"blame",
|
|
27
|
+
"archaeology",
|
|
28
|
+
"why",
|
|
29
|
+
"mcp",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"codex",
|
|
33
|
+
"cursor",
|
|
34
|
+
"context",
|
|
35
|
+
"knowledge-base",
|
|
36
|
+
"code-review",
|
|
37
|
+
"onboarding"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/carefreebit/whygit.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/carefreebit/whygit#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/carefreebit/whygit/issues"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"author": "lxh"
|
|
52
|
+
}
|