@davesheffer/hunch 0.15.3 β†’ 0.16.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 CHANGED
@@ -18,7 +18,17 @@ cd your-repo && hunch init && hunch backfill --since 90d
18
18
  hunch why src/some/file.ts # …or just ask Claude Code: "why is X built this way?"
19
19
  ```
20
20
 
21
- <sub>Works with **Claude Code, Cursor, Copilot & Windsurf** from one shared graph. Full walkthrough in [Getting started](#getting-started).</sub>
21
+ <sub>Works with **Claude Code, Cursor, Copilot & Windsurf** from one shared graph.</sub>
22
+
23
+ ### πŸ“š **[Read the full documentation β†’ hunch-pi.vercel.app/docs](https://hunch-pi.vercel.app/docs)**
24
+
25
+ The docs site is the complete reference β€” setup, every CLI command and MCP tool, the
26
+ guards, troubleshooting. This README is the tour. Jump to:
27
+ [Install](https://hunch-pi.vercel.app/docs#install) Β·
28
+ [MCP setup](https://hunch-pi.vercel.app/docs#mcp) Β·
29
+ [Firmness](https://hunch-pi.vercel.app/docs#firmness) Β·
30
+ [CLI reference](https://hunch-pi.vercel.app/docs#cli) Β·
31
+ [Troubleshooting](https://hunch-pi.vercel.app/docs#doctor)
22
32
 
23
33
  ## The problem
24
34
 
@@ -48,385 +58,123 @@ came before. Local-first, no documentation toil, no SaaS.
48
58
  - **Learn**: each commit becomes a structured **Decision** (an ADR); a failing test
49
59
  becomes a **Bug** with a ranked suspect list; recurring or severe bugs are promoted
50
60
  into **Constraints** (do-not-break invariants) and raise a component's *fragility*.
51
- - **Ground**: Claude Code reads it through an **MCP server**, an auto-maintained
61
+ - **Ground**: any MCP assistant reads it through an **MCP server**, an auto-maintained
52
62
  **`CLAUDE.md`**, and **slash commands** β€” every answer cites `provenance`
53
63
  (source + confidence + evidence), so nothing is a blind assertion.
54
64
 
55
- ## Getting started
56
-
57
- ### 1. Install it
58
-
59
- ```bash
60
- npm install -g @davesheffer/hunch # puts `hunch` on your PATH
61
- ```
62
-
63
- Or run from source (for hacking on Hunch itself):
65
+ β†’ Concepts in depth: [the reasoning graph](https://hunch-pi.vercel.app/docs#graph) Β·
66
+ [provenance](https://hunch-pi.vercel.app/docs#provenance) Β·
67
+ [time-travel](https://hunch-pi.vercel.app/docs#time-travel)
64
68
 
65
- ```bash
66
- npm install
67
- npm run build # compiles to dist/
68
- npm link # optional: puts a global `hunch` on your PATH
69
- ```
70
-
71
- Either way you then type `hunch …`. From a source checkout without `npm link`, use
72
- `node dist/cli/index.js …` (or `npm run hunch -- …` to run via tsx). The rest of this
73
- README uses `hunch` for brevity.
74
-
75
- ### 2. (Recommended) make a coding-assistant CLI available
76
-
77
- Hunch's LLM synthesis is billed to **your subscription** through a coding-assistant
78
- CLI β€” **never** a pay-per-token API key (the API key is stripped from the child env).
79
- Hunch auto-detects the first one present, in this order:
80
-
81
- | CLI | Subscription | Detected by |
82
- |---|---|---|
83
- | `claude` (Claude Code) | Claude Pro/Max | `claude --version` |
84
- | `codex` (OpenAI Codex) | ChatGPT Plus/Pro | `codex --version` |
85
- | `cursor-agent` (Cursor) | Cursor | `cursor-agent --version` |
86
-
87
- If none is installed, Hunch still works using a deterministic structural heuristic
88
- (lower-confidence drafts). `hunch doctor` tells you which mode you're in; force one
89
- with `HUNCH_SYNTH_PROVIDER=claude-cli|codex-cli|cursor-agent|deterministic`.
90
-
91
- ### 3. Initialize the repo you want a memory for
92
-
93
- ```bash
94
- hunch init # scaffold .hunch/, index, install the post-commit hook,
95
- # write .mcp.json + slash commands + CLAUDE.md, register the merge driver
96
- hunch backfill --since 90d # cold start: seed decisions from recent git history
97
- ```
98
-
99
- `init` writes a `.mcp.json` pointing at *this machine's* node + Hunch β€” so **reload
100
- Claude Code in the repo** afterward to pick up the `hunch_*` tools. (Each teammate runs
101
- `hunch init` once to wire up their own clone; the captured `.hunch/` content is shared
102
- via git.)
103
-
104
- > **Prefer `hunch init` (project-local `.mcp.json`) over a global `claude mcp add`.**
105
- > `.mcp.json` is registered by file path, so it's robust. A global `claude mcp add`
106
- > writes to `~/.claude.json` keyed by the raw working-directory string β€” and **on
107
- > Windows** that's a trap: drive letters are case-insensitive (`c:\` and `C:\` are the
108
- > same folder) but Claude Code compares the key case-sensitively, so it can create two
109
- > project blocks for one directory and a session that resolves to the *other* casing
110
- > sees no `hunch_*` tools (registration looked fine, the tools just aren't there). If
111
- > you hit this, run **`hunch doctor`** β€” on Windows it detects the split and heals it
112
- > (merging the MCP servers across both casings, after backing up `~/.claude.json`).
113
- > `hunch init` runs the same heal automatically at the end.
114
-
115
- ### 4. Use it
116
-
117
- ```bash
118
- hunch why src/auth/session.ts # the decisions / bugs / invariants behind a file
119
- hunch doctor # check git, schema version, and synthesis mode
120
- ```
121
-
122
- …and in Claude Code, just ask: *"why is the session module built this way?"*
123
-
124
- ## Two ways to use it
125
-
126
- **Through Claude Code (the point).** Once the MCP server is registered, ask questions
127
- normally and Claude consults Hunch, or invoke the slash commands:
128
-
129
- | Slash command | What it does |
130
- |---|---|
131
- | `/hunch-why <file\|symbol>` | the decisions, invariants, and bug history behind it β€” with citations |
132
- | `/hunch-fix <bug>` | fix a bug grounded in past root causes, blast radius, and constraints |
133
- | `/hunch-fragile` | a fragility report (the riskiest code, with evidence) |
134
-
135
- The MCP tools Claude calls under the hood: `hunch_why`, `hunch_query`,
136
- `hunch_check_constraints`, `hunch_get_dependents` (blast radius), `hunch_blast_radius`
137
- (dependent files + near-violations a change could break indirectly), `hunch_bug_lineage`,
138
- `hunch_context` (surgical minimal slice for a task), `hunch_timeline` (a target's decision
139
- history over time), `hunch_merge_verdict` (a cited BLOCK/WARN/PASS over a diff β€” see below),
140
- and the write-backs `hunch_record_decision`, `hunch_record_correction` (turn a correction into
141
- an enforced invariant β€” see below). `hunch_why` and `hunch_context` take an optional `as_of`
142
- (commit/tag/branch) to **time-travel** the graph to a past state.
143
-
144
- ### Works with any MCP assistant
145
-
146
- The Hunch MCP server is **client-agnostic** β€” one `.hunch/` graph powers every
147
- assistant. `hunch init` scaffolds each tool's MCP config + ambient grounding so
148
- they all consult the same memory:
149
-
150
- | Assistant | MCP config | Grounding file |
151
- |---|---|---|
152
- | Claude Code | `.mcp.json` | `CLAUDE.md` + `/hunch-*` slash commands |
153
- | Cursor | `.cursor/mcp.json` | `.cursor/rules/hunch.mdc` (always-applied) |
154
- | VS Code (Copilot) | `.vscode/mcp.json` | `.github/copilot-instructions.md` |
155
- | Codex CLI | `.codex/config.toml` | `AGENTS.md` |
156
- | Windsurf | `.windsurf/mcp_config.json` | `.windsurf/rules/hunch.md` (always-on) |
157
- | Anything else | β€” | `AGENTS.md` (cross-tool standard) |
158
-
159
- Each writer **merges** into existing files (other MCP servers and your own prose are
160
- preserved) and is idempotent. Opt out with `hunch init --no-providers`.
161
-
162
- **Through the CLI** β€” the same graph, from your terminal:
163
-
164
- | Command | What |
165
- |---|---|
166
- | `hunch init` | scaffold `.hunch/`, index, install hook + merge driver, auto-install the advisory pre-commit guard, install the **Claude Code agent hooks**, and wire up **every assistant** (Claude Code, Cursor, VS Code/Copilot, Codex, Windsurf, AGENTS.md). Flags: `--no-enforce`, `--enforce-strict`, `--no-providers`, `--no-agent-hooks`, `--firmness <level>` |
167
- | `hunch index` | parse repo β†’ symbols / edges / components (deterministic, no LLM) |
168
- | `hunch backfill --since 90d` | replay git history β†’ seed decisions |
169
- | `hunch sync [sha]` | turn a commit into a Decision (run automatically by the hook) |
170
- | `hunch record-bug --test <id> --message <m>` | capture a Bug from a failing test |
171
- | `hunch record-constraint "<statement>" [--scope <globs>] [--severity advisory\|warning\|blocking] [--type …] [--rationale <t>] [--source-decision <id>]` | record an invariant the code must not break (what `hunch check` + the strict agent hook enforce) |
172
- | `hunch firmness [off\|advisory\|firm\|strict]` | get/set how firmly the agent hook enforces Hunch before edits (no arg prints the current level) |
173
- | `hunch test [cmd…]` | run the suite (default `npm test`); auto-capture failures as Bugs (suspects + recurrenceβ†’Constraints), mark passing tests' bugs fixed |
174
- | `hunch why <path\|symbol> [--as-of <ref>]` | decisions / bugs / constraints explaining a target (flags `⚠STALE`); `--as-of` time-travels to what was believed at a commit/tag/branch |
175
- | `hunch timeline <path\|symbol>` | the decision history for a target β€” what was believed, its valid-time window, and what superseded it |
176
- | `hunch supersede <old> --by <new>` | mark one decision as replaced by another: closes the old one's valid-time window (invalidate, don't delete) |
177
- | `hunch query "<q>" [--semantic]` | full-text + graph search (`--semantic` blends in local embeddings) |
178
- | `hunch embed` | generate local embeddings for semantic recall (opt-in; needs `@huggingface/transformers`) |
179
- | `hunch context <path\|symbol> [--as-of <ref>]` | minimal relevant slice for a task: invariants β†’ decisions β†’ bugs β†’ blast radius (`--as-of` time-travels) |
180
- | `hunch fragile` | ranked fragility report with evidence |
181
- | `hunch check [--staged\|--commit <sha>\|--base <ref>] [--strict] [--format text\|markdown] [--blast]` | guardrail: flag changes touching a do-not-break invariant **directly or via blast radius** (a guarded file that depends on what you changed), **and changes that re-introduce something a decision deliberately retired** (the Regression Guard). `--base <ref>` checks a PR's diff (for CI); `--format markdown` emits a PR comment; `--strict` fails only on a direct, high-confidence, non-stale **blocking** invariant; `--blast` prints the dependency fan-out |
182
- | `hunch ci` | scaffold the **CI Constraint Guard** β€” a GitHub Action that runs `hunch check` on every PR, comments the affected invariants/decisions, and fails on a blocking one |
183
- | `hunch stale [--resync]` | drift: records whose files changed after last verification (`--resync` regenerates stale decisions from their commits) |
184
- | `hunch review [--accept <id>\|--reject <id>]` | curate: triage / promote / drop low-confidence drafts |
185
- | `hunch migrate` | upgrade `.hunch/` records to the current schema version |
186
- | `hunch compact [--apply]` | prune low-value drafts to bound growth (dry-run by default) |
187
- | `hunch doctor` | environment diagnostics (git, auth mode, schema version, counts) |
188
- | `hunch mcp` | start the MCP server over stdio (Claude Code connects here) |
189
-
190
- ## Grounding the agent automatically (firmness)
191
-
192
- Telling an assistant "consult Hunch first" in a prompt is advisory β€” it drifts. `hunch
193
- init` instead installs two **Claude Code agent hooks** (in `.claude/settings.json`) so the
194
- grounding is enforced by the harness, not by the model's memory:
195
-
196
- - **Before every edit** (`PreToolUse` on `Edit`/`Write`/`MultiEdit`) Hunch injects the
197
- relevant slice for the file being touched β€” its decisions, invariants, bug history, and
198
- blast radius β€” straight into the model's context.
199
- - **On every prompt** (`UserPromptSubmit`) it reminds the agent to query Hunch.
200
-
201
- How hard it pushes is one committed knob β€” set it once, it applies to the whole team:
69
+ ## Getting started
202
70
 
203
71
  ```bash
204
- hunch firmness # print the current level
205
- hunch firmness strict # change it (takes effect on the next edit; no restart)
206
- ```
207
-
208
- | Level | Before an edit |
209
- |---|---|
210
- | `off` | nothing (hook is a no-op) |
211
- | `advisory` *(default)* | inject the relevant Hunch slice as context |
212
- | `firm` | advisory **+** explicitly flag invariants in the file's scope |
213
- | `strict` | firm **+** **deny** an edit that hits a *blocking* invariant (directly or via blast radius), feeding the invariant back as the refusal reason |
214
-
215
- Before an edit, the hook also grounds the agent in anything an in-force decision
216
- **deliberately retired** from that file ("don't re-introduce `login` here β€” dec_017 removed
217
- it"). The actual gate is at commit time: `hunch check` runs the **Regression Guard** over
218
- the staged diff and, under `--strict`, fails the commit when a change re-adds a retired
219
- symbol/dependency tied to a blocking invariant (otherwise it warns).
220
-
221
- The hook never breaks your flow: any error or unrecognized input emits nothing and exits
222
- 0, and it stays silent on files Hunch hasn't learned yet. `strict` only bites once you have
223
- **blocking** constraints recorded (`hunch record-constraint … --severity blocking`) β€” with
224
- none, every level degrades to context-only. Opt out of the hooks entirely with `hunch init
225
- --no-agent-hooks`.
226
-
227
- ## Never Twice: corrections become enforced invariants
228
-
229
- The most expensive failure in AI coding is being corrected and then *re-corrected* β€” you
230
- tell the agent "no, never call the pay-per-token API here," it complies once, and next
231
- session it does it again because the feedback was stored as advisory text, not enforced.
232
-
233
- Hunch closes that loop. When you correct the agent, it captures the rule as a **first-class
234
- Constraint** (provenance `human_confirmed`) via the `hunch_record_correction` MCP tool β€” and
235
- from then on the **same pre-edit hook + CI Constraint Guard** hold *every* assistant to it:
236
-
237
- ```text
238
- You: "no β€” never import lodash, we ship our own utils"
239
- Agent: calls hunch_record_correction({ rule: "never import lodash; use src/utils",
240
- scope_hint_file: "src/cart.ts", severity: "blocking" })
241
- β†’ con_… recorded. A later edit that adds `import _ from "lodash"` to that scope is DENIED
242
- (strict firmness) and the PR fails CI β€” in Cursor, Copilot, Windsurf, or Claude Code alike.
243
- ```
244
-
245
- The `UserPromptSubmit` hook nudges the agent to persist a rule whenever your prompt reads
246
- like a correction ("no…", "that's wrong", "never do X"), so capture is one frictionless step
247
- rather than a discipline. Scoping is conservative by default (the file you were in); a
248
- repo-wide (`**`) rule is only blocking when you pass `applies_to_all`, so one correction
249
- can't silently gate the whole tree. Because it's the *same* constraint machinery, a
250
- correction is enforced exactly like a hand-authored invariant β€” see firmness above.
251
-
252
- ## Causal Merge Verdict: does this change re-open a closed bug?
253
-
254
- A diff-only reviewer (CodeRabbit, Greptile) sees *what* changed. It can't see that the line
255
- you're deleting is the fix for an incident, or that the symbol you're re-adding was
256
- deliberately retired. Hunch can β€” because it holds the **why**.
257
-
258
- `hunch_merge_verdict` (MCP tool) and `hunch check` replay a diff against the graph and return
259
- one verdict β€” **BLOCK / WARN / PASS** β€” that *cites the reasoning*, not just the rule:
72
+ npm install -g @davesheffer/hunch # Node β‰₯ 20; puts `hunch` on your PATH
73
+ cd your-repo
74
+ hunch init # scaffold .hunch/, index, install hooks, wire up assistants
75
+ hunch backfill --since 90d # cold start: seed decisions from recent git history
76
+ hunch why src/auth/session.ts # …then ask your assistant: "why is X built this way?"
77
+ ```
78
+
79
+ `hunch init` scaffolds `.hunch/`, indexes the repo, installs the git hooks + merge driver,
80
+ writes `.mcp.json` + slash commands + an auto-maintained `CLAUDE.md`, and wires up **every
81
+ detected assistant** (Claude Code, Cursor, VS Code/Copilot, Windsurf, Codex) to the same
82
+ graph β€” merging idempotently into existing files. **Reload your assistant in the repo**
83
+ afterward to pick up the `hunch_*` tools. Each teammate runs `hunch init` once; the
84
+ `.hunch/` content is shared via git.
85
+
86
+ > Synthesis is billed to **your coding-assistant subscription** (Claude/Codex/Cursor CLI),
87
+ > **never** a pay-per-token API key β€” and falls back to a deterministic heuristic if no CLI
88
+ > is present. Details: [Synthesis & billing](https://hunch-pi.vercel.app/docs#synthesis).
89
+ > On Windows, prefer `hunch init` over a global `claude mcp add`; if tools don't appear,
90
+ > `hunch doctor` heals it ([why](https://hunch-pi.vercel.app/docs#windows)).
91
+
92
+ **Full walkthrough β†’** [Getting started](https://hunch-pi.vercel.app/docs#install) Β·
93
+ [MCP & assistants](https://hunch-pi.vercel.app/docs#mcp) Β·
94
+ [MCP tools](https://hunch-pi.vercel.app/docs#mcp-tools) Β·
95
+ [slash commands](https://hunch-pi.vercel.app/docs#slash) Β·
96
+ [the 22-command CLI](https://hunch-pi.vercel.app/docs#cli)
97
+
98
+ ## Enforcement: memory that holds the line
99
+
100
+ Hunch isn't just recall β€” it's a set of **guards** that stop the AI (and you) from undoing
101
+ intentional design. All ride the same rails: the pre-edit hook, `hunch check`, the
102
+ `hunch_merge_verdict` MCP tool, and the CI Constraint Guard. How hard they push is one
103
+ committed knob β€” [**firmness**](https://hunch-pi.vercel.app/docs#firmness)
104
+ (`off` β†’ `advisory` β†’ `firm` β†’ `strict`).
105
+
106
+ ### Never Twice β€” corrections become enforced invariants
107
+
108
+ You tell the agent "no, never call the pay-per-token API here," it complies once, and next
109
+ session it does it again β€” because the feedback was stored as advisory text. Hunch closes
110
+ that loop: a correction is captured as a first-class **Constraint** (`human_confirmed`) via
111
+ `hunch_record_correction`, and from then on the **same hook + CI guard** hold *every*
112
+ assistant to it. β†’ [docs](https://hunch-pi.vercel.app/docs#never-twice)
113
+
114
+ ### Causal Merge Verdict β€” does this change re-open a closed bug?
115
+
116
+ A diff-only reviewer sees *what* changed; it can't see that the line you're deleting is the
117
+ fix for an incident. Hunch can β€” `hunch_merge_verdict` replays a diff against the graph and
118
+ returns a cited **BLOCK / WARN / PASS**:
260
119
 
261
120
  ```text
262
121
  VERDICT: β›” BLOCK β€” this change breaks a recorded invariant or re-opens a known bug.
263
122
 
264
123
  β›” pay() must verify the session before charging β€” con_pay
265
124
  🧠 why: "Charge must verify the session first" (dec_pay)
266
- 🐞 guards against: Double-charge on unverified session β€” pay() charged without verifying (bug_trunc)
267
- ```
268
-
269
- It's **deterministic** (no LLM): for every invariant *directly* in scope it walks
270
- constraint β†’ `source_decision` β†’ the bug whose root cause spawned it; it flags invariants
271
- reached transitively (blast radius, advisory) and any deliberately-retired code the diff
272
- re-introduces. BLOCK fires only on a direct, high-confidence, non-stale **blocking** invariant
273
- or a blocking-linked regression β€” near-hits stay advisory, so it's safe as a merge gate. Call
274
- it before opening a PR (`{}` checks staged changes; pass `base: "origin/main"` for the range);
275
- the CI Constraint Guard renders the same cited verdict as a PR comment.
276
-
277
- ## Veto: re-introducing a *rejected* approach is blocked
278
-
279
- The Regression Guard catches re-adding code a decision deliberately **retired**. But the most
280
- expensive reversal is re-introducing an approach a decision **rejected** β€” the
281
- `alternatives_rejected` that *never existed in code*, so a diff-only reviewer (and the regression
282
- guard) is blind to it. A fresh session, not knowing, re-adds the very dependency you rejected for
283
- latency last month.
284
-
285
- **Veto** closes that gap. A decision can carry **tripwires** β€” machine-checkable signals (a
286
- forbidden dependency, symbol, or scoped pattern) for an `alternatives_rejected` entry. When a diff
287
- re-introduces one, Hunch blocks it with the receipt:
288
-
289
- ```text
290
- β›” VETO β€” this reverses dec_49916d02c9 ("Read-only layer over committed .hunch/ JSON").
291
- You rejected: "extension queries MCP/API server for data" (adds latency, runtime coupling)
292
- You chose: read directly from committed JSON, no backend dependency.
293
- evidence: +import axios (vscode-extension/src/extension.ts)
294
- ```
295
-
296
- It rides the **same rails** as everything else: `CheckReport.vetoes` lights up `hunch check`, the CI
297
- guard, and `hunch_merge_verdict` together, and the pre-edit hook denies the live edit (Edit / Write /
298
- MultiEdit) *before* it's staged β€” so the agent self-corrects with no human in the loop. Enforcement
299
- is **deterministic** (a set-intersection over a human-vouched record β€” no model in the block path)
300
- and **progressive**: an auto-drafted tripwire only *warns*; `hunch veto backfill` drafts them and
301
- `hunch review --accept` confirms a decision **and** its tripwires, flipping it from advisory to
302
- blocking in one keypress. Full design + DX: [docs/veto.md](docs/veto.md).
303
-
304
- ## Semantic search (optional)
305
-
306
- By default `hunch query` and the `hunch_query` MCP tool use fast keyword (FTS) search β€”
307
- zero setup, instant, offline. For recall on *paraphrases* (a question that shares no words
308
- with the record it should find), opt into **local embeddings**:
309
-
310
- ```bash
311
- npm i -g @huggingface/transformers # one-time; a local model runtime
312
- hunch embed # embed your records (first run downloads ~90MB)
313
- hunch query --semantic "auth token expiry" # hybrid keyword + semantic recall
125
+ 🐞 guards against: Double-charge on unverified session (bug_…)
314
126
  ```
315
127
 
316
- > **Install it where `hunch` runs.** The runtime is resolved from `hunch`'s own
317
- > `node_modules`, so match the install scope: a globally-installed `hunch` needs the
318
- > global (`-g`) install above; running from a source checkout needs it in the repo
319
- > (`npm i @huggingface/transformers` there). If `hunch embed` reports the model "present
320
- > but failed to load," the scopes don't match. `hunch doctor` shows the active mode.
128
+ Deterministic (no LLM), and safe as a merge gate β€” it blocks only on a direct,
129
+ high-confidence, non-stale blocking invariant. β†’ [docs](https://hunch-pi.vercel.app/docs#merge-verdict)
321
130
 
322
- Embeddings are **local and free** (no API β€” consistent with the subscription-only synthesis
323
- rule) and **opt-in** (the base install stays lean). The long-lived MCP server picks them up
324
- automatically once present. Vectors live in the derived SQLite index and are reconciled by
325
- content hash on every `hunch index`, so they never drift from the JSON source of truth. `hunch
326
- doctor` reports coverage; tune the blend with `HUNCH_RRF_W_FTS` / `HUNCH_RRF_W_SEM` / `HUNCH_RRF_K`.
131
+ ### Decision Guard (Veto) β€” re-introducing a *rejected* approach is blocked
327
132
 
328
- ## What makes the capture good (not just "changed N files")
133
+ The most expensive reversal is re-adding an approach a decision **rejected** (latency, a
134
+ forbidden dependency) β€” code that never existed, so a diff reviewer is blind to it. A
135
+ decision carries machine-checkable **tripwires**; re-introduce one and Hunch blocks it with
136
+ the receipt of what you rejected and why. β†’ [docs](https://hunch-pi.vercel.app/docs#veto)
329
137
 
330
- Even with **no LLM**, the write path runs a structured **diff analysis** β€” added /
331
- removed / changed symbols, new and dropped dependencies, and which invariants a change
332
- touches β€” so an auto-captured decision reads like *"introduced `verifySession`,
333
- `revokeSession`; removed `login`; new dep: redis; touches con_004"*, with breaking-change
334
- consequences. With the `claude` CLI present it upgrades to full LLM synthesis; otherwise
335
- it stays useful offline. Either way every record is **advisory and cheap to discard** β€”
336
- `hunch review` lets you promote the good ones to human-confirmed.
138
+ Plus the **Regression Guard** (re-adding deliberately-retired code) and the
139
+ **[CI Constraint Guard](https://hunch-pi.vercel.app/docs#ci)** (`hunch ci` β€” a PR gate that
140
+ comments the affected `con_`/`dec_` ids and fails on a blocking one).
337
141
 
338
142
  ## Working as a team
339
143
 
340
- The `.hunch/` JSON is the **source of truth**: diffable, reviewable in PRs, and synced
341
- for free over `git push` / `pull`. `hunch init` also registers a **git merge driver** so
342
- concurrent edits to the graph merge **by record id** instead of throwing conflict markers
343
- (human-confirmed beats auto, then higher confidence, then recency). The routing lives in a
344
- committed `.gitattributes`; the per-clone driver definition is set up by each teammate's
345
- `hunch init`. The graph is **OS-agnostic**: paths are stored and matched in POSIX form, and
346
- an installed Hunch registers its MCP server by package name (`npx -y @davesheffer/hunch`)
347
- rather than an absolute path β€” so a teammate on Windows, macOS, or Linux shares the same
348
- `.hunch/` and the same committed config without per-machine fixups.
144
+ The `.hunch/` JSON is the **source of truth** β€” diffable, reviewable in PRs, synced for free
145
+ over `git push` / `pull`. `hunch init` registers a git **merge driver** so concurrent edits
146
+ merge **by record id** (human-confirmed beats auto, then confidence, then recency). The graph
147
+ is **OS-agnostic**: paths are stored in POSIX form and an installed Hunch registers its MCP
148
+ server by package name, so Windows / macOS / Linux teammates share one memory without
149
+ per-machine fixups. β†’ [docs](https://hunch-pi.vercel.app/docs#team)
349
150
 
350
151
  ## Continuous learning (CI)
351
152
 
352
- The decision half of the loop is automatic (the post-commit hook). Light up the **bug /
353
- constraint half** by wrapping your test run with `hunch test`:
153
+ The decision half of the loop is automatic (the post-commit hook). Light up the bug/constraint
154
+ half by wrapping your test run β€” it captures failures as **Bugs** (recurrences auto-promote
155
+ **Constraints**) and resolves fixed ones, preserving the runner's exit code:
354
156
 
355
157
  ```bash
356
- hunch test # runs `npm test`; capture failures β†’ Bugs, resolve fixed ones
357
- hunch test -- pytest -q # any runner: pass the command after `--`
158
+ hunch test # runs `npm test`; any runner: hunch test -- pytest -q
358
159
  ```
359
160
 
360
- It parses TAP and the `node:test` spec reporter, captures each failing test as a **Bug**
361
- (ranked suspects; a recurrence or substantiated high-severity failure auto-promotes a
362
- do-not-break **Constraint**), and marks a previously-open bug **fixed** once its test passes
363
- again. It preserves the runner's exit code, so it's a drop-in CI step:
364
-
365
- ```yaml
366
- # .github/workflows/ci.yml
367
- - run: npm ci
368
- - run: npx hunch test # exits non-zero on failure, just like the suite
369
- - run: | # persist what was learned (optional)
370
- git add .hunch && git commit -m "chore(hunch): capture test run" || true
371
- git push || true
372
- ```
373
-
374
- Repair drift after refactors with **`hunch stale --resync`** (re-synthesizes stale decisions
375
- from their commits via the LLM).
376
-
377
- ### Block a PR that breaks memory (CI Constraint Guard)
378
-
379
- Memory that only *advises* gets ignored. `hunch ci` scaffolds a GitHub Action that turns
380
- Hunch into a **merge gate**: on every pull request it runs `hunch check` over the diff,
381
- posts a sticky comment citing the affected `con_`/`dec_` ids, and **fails the check** when
382
- the PR breaks a *direct, high-confidence, non-stale* blocking invariant, re-adds
383
- deliberately-retired code, or contradicts an in-force decision.
384
-
385
- ```bash
386
- hunch ci # writes .github/workflows/hunch-guard.yml β€” commit it
387
- ```
388
-
389
- It reasons over **the diff plus the constraints committed in the same git history**, so the
390
- comment says exactly which decision a change violates ("breaks `con_004` β€” server-side
391
- revocation, from `dec_017`"). Make *Hunch Guard* a required status check in branch protection
392
- to enforce on merge. The hardened strict gate only blocks on high-confidence, non-stale
393
- invariants β€” stale / low-confidence / blast-radius hits stay advisory in the comment β€” so
394
- it's safe to require on a shared repo. Under the hood it's just
395
- `hunch check --base origin/<target> --strict --format markdown`.
396
-
397
- ## Maintenance
161
+ Drop `npx hunch test` into CI, and `hunch ci` to scaffold the PR merge gate.
162
+ β†’ [docs](https://hunch-pi.vercel.app/docs#ci)
398
163
 
399
- - **`hunch doctor`** β€” is git healthy? are you on the subscription path or the offline
400
- heuristic? what schema version is on disk? how many records?
401
- - **`hunch migrate`** β€” after upgrading Hunch, bring old `.hunch/` records up to the
402
- current schema (old records are migrated in memory on every read, so reads never break;
403
- `migrate` persists the upgrade and never drops a record it can't migrate).
404
- - **`hunch compact --apply`** β€” auto-captured drafts accumulate; compaction prunes the
405
- low-value ones (rejected / superseded / stale drafts, resolved low-confidence bugs).
406
- It **never** removes an accepted/human-confirmed decision, an open bug, a constraint, or
407
- any record another record still references. Run without `--apply` first to preview.
164
+ ## Semantic search (optional)
408
165
 
409
- ## Where it's stored
166
+ `hunch query` uses fast keyword search out of the box. For recall on paraphrases, opt into
167
+ **local embeddings** (`npm i -g @huggingface/transformers && hunch embed`) β€” local, free, and
168
+ opt-in. Vectors live in the derived SQLite index and never drift from the JSON source of truth.
410
169
 
411
- ```
412
- .hunch/
413
- β”œβ”€ components/ one JSON file per architecture node (curated, PR-reviewable)
414
- β”œβ”€ decisions/ one JSON file per Decision (ADR)
415
- β”œβ”€ bugs/ one JSON file per Bug
416
- β”œβ”€ constraints/ one JSON file per Constraint (invariant)
417
- β”œβ”€ symbols/index.json the symbol graph (high-cardinality, single file)
418
- β”œβ”€ edges/index.json the dependency graph
419
- β”œβ”€ manifest.json on-disk schema version
420
- └─ hunch.sqlite DERIVED FTS5 + graph index, rebuilt by `hunch index` (gitignored)
421
- ```
422
-
423
- Low-volume entities are one file per record so they read cleanly in a PR; the
424
- high-cardinality symbol/edge graphs are single id-sorted arrays to keep git noise down.
425
- SQLite is a throwaway index rebuilt from the JSON β€” only the JSON is committed.
170
+ ## VS Code
426
171
 
427
- > Note: the on-disk directory is still `.hunch/` (and the MCP tools are still `hunch_*`)
428
- > for backward compatibility with existing graphs. A future release may migrate these to
429
- > `.hunch/` / `hunch_*`.
172
+ A companion **[VS Code extension](vscode-extension/)** (on
173
+ [Open VSX](https://open-vsx.org/extension/davesheffer/hunch-vscode) β€” VS Code / Cursor /
174
+ Windsurf / VSCodium) brings the graph into the editor: a tree of decisions / invariants /
175
+ bugs / bug-lineage / fragility / stale records, CodeLens summaries, hover with bug history,
176
+ invariants in the Problems panel, an interactive component graph, and a status-bar invariant
177
+ counter. It reads the committed `.hunch/` JSON directly; writes delegate to the `hunch` CLI.
430
178
 
431
179
  ## Architecture
432
180
 
@@ -435,51 +183,39 @@ src/
435
183
  β”œβ”€ core/ types (Zod schema), ids, paths, glob, schema migration, atomic file I/O
436
184
  β”œβ”€ store/ JSON source of truth ←→ SQLite/FTS5 derived index; merge driver; compaction
437
185
  β”œβ”€ extractors/ tree-sitter parse, git introspection, the indexer
438
- β”œβ”€ synthesis/ write path: Claude-CLI (subscription) or deterministic fallback
186
+ β”œβ”€ synthesis/ write path: subscription CLI (Claude/Codex/Cursor) or deterministic fallback
439
187
  β”œβ”€ mcp/ MCP stdio server (the hunch_* tools)
440
188
  β”œβ”€ integrations/ post-commit hook, CLAUDE.md writer, .mcp.json + slash commands, merge driver
441
189
  └─ cli/ commander entrypoint
442
190
  ```
443
191
 
444
- ## VS Code
445
-
446
- A companion **[VS Code extension](vscode-extension/)** (on
447
- [Open VSX](https://open-vsx.org/extension/davesheffer/hunch-vscode) β€” works in
448
- VS Code / Cursor / Windsurf / VSCodium) brings the graph into the editor: a tree of
449
- decisions / invariants / bugs / **bug-lineage** / fragility / **stale records**, a
450
- **CodeLens** summary + per-symbol bug/fragility marks, **hover** with bug history,
451
- invariants surfaced in the **Problems panel**, overview-ruler hotspot marks, an
452
- interactive **component graph**, fuzzy **search**, and a status-bar invariant counter.
453
- It reads the committed `.hunch/` JSON directly (no server, no native deps); writes
454
- delegate to the `hunch` CLI.
192
+ Everything lives under `.hunch/` as git-tracked JSON (the source of truth); SQLite is a
193
+ throwaway derived index. β†’ [storage layout](https://hunch-pi.vercel.app/docs#storage) Β·
194
+ [DESIGN.md](DESIGN.md) for the full conceptual model.
455
195
 
456
196
  ## Notable engineering decisions
457
197
 
458
198
  - **Subscription-billed synthesis, never the API.** The write path drives your Claude
459
199
  subscription via the `claude` CLI; `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` are
460
- stripped from the child env to force subscription auth (they outrank it in headless
461
- mode). A deterministic, no-LLM fallback means the loop never hard-requires credentials.
462
- - **Native `tree-sitter` (0.21.1) + `tree-sitter-typescript`, not web-tree-sitter.** The
463
- prebuilt WASM grammars have an ABI incompatible with current `web-tree-sitter`; the
464
- native bindings ship Node-20 prebuilds (no compiler) and a simpler synchronous API.
465
- - **`better-sqlite3` pinned to `12.9.0`** β€” 12.10.x ships no Node-20 prebuild and would
466
- force a source compile; 12.9.0 has the Node-20 (ABI 115) prebuild.
467
- - **Atomic, durable writes.** All `.hunch/` writes go through a temp-file + rename, with a
468
- Windows-safe fallback, so an interrupted write can't truncate the index; `put`/`delete`
469
- refuse to rewrite a corrupt index rather than flatten it.
470
- - **OS-agnostic by construction.** Git emits `/`-separated paths on every platform, so the
471
- graph is the source of truth in POSIX form; any free-form target is canonicalized before
472
- comparison, so `src\auth\x.ts` and `src/auth/x.ts` resolve to the same records. Generated
473
- configs that get committed reference Hunch by package name, never a machine-local path.
200
+ stripped from the child env to force subscription auth. A deterministic, no-LLM fallback
201
+ means the loop never hard-requires credentials.
202
+ - **Native `tree-sitter` (0.21.1)**, not web-tree-sitter β€” the prebuilt WASM grammars have an
203
+ incompatible ABI; the native bindings ship Node-20 prebuilds and a simpler synchronous API.
204
+ - **`better-sqlite3` pinned to `12.9.0`** β€” 12.10.x ships no Node-20 prebuild.
205
+ - **Atomic, durable writes.** All `.hunch/` writes go through temp-file + rename; an
206
+ interrupted write can't truncate the index, and `put`/`delete` refuse to rewrite a corrupt one.
207
+ - **OS-agnostic by construction.** Paths are canonicalized to POSIX before comparison, and
208
+ committed configs reference Hunch by package name, never a machine-local path.
474
209
 
475
210
  ## Develop
476
211
 
477
212
  ```bash
478
- npm run typecheck # tsc --noEmit
479
- npm test # node:test suite (store, graph, parse, indexer, synthesis, migrate, merge, compact)
480
- npm run hunch -- why src/store/hunchStore.ts # run the CLI from source via tsx, no build
213
+ npm run dev -- <args> # run the CLI from source via tsx (no build step)
214
+ npm run typecheck # strict tsc β€” the gate
215
+ npm test # node:test suite
216
+ npm run build # compile to dist/ (the published artifact)
481
217
  ```
482
218
 
483
- See [DESIGN.md](DESIGN.md) for the full spec. PR/CI enforcement now ships as the
484
- **CI Constraint Guard** (`hunch ci`). Still deferred by design: a hosted web dashboard
485
- and multi-repo support.
219
+ Hunch is pure TypeScript ESM, Node β‰₯ 20, licensed **Apache-2.0**. See
220
+ [CONTRIBUTING.md](CONTRIBUTING.md), [DESIGN.md](DESIGN.md), and the full
221
+ [developer docs](https://hunch-pi.vercel.app/docs#develop).
package/dist/cli/index.js CHANGED
@@ -15,11 +15,11 @@
15
15
  */
16
16
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { execFileSync, spawnSync } from "node:child_process";
18
- import { join, relative, dirname } from "node:path";
19
- import { fileURLToPath } from "node:url";
18
+ import { relative } from "node:path";
20
19
  import { Command } from "commander";
21
20
  import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
22
21
  import { looksLikeCorrection, CORRECTION_NUDGE } from "../core/correction.js";
22
+ import { HUNCH_VERSION } from "../core/version.js";
23
23
  import { HunchStore } from "../store/hunchStore.js";
24
24
  import { selectEmbedder } from "../store/embedder.js";
25
25
  import { indexRepo } from "../extractors/indexer.js";
@@ -45,20 +45,6 @@ import { readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js"
45
45
  import { mergeHunchJson } from "../store/merge.js";
46
46
  import { planCompaction } from "../store/compact.js";
47
47
  import { resolveInvocation } from "./invocation.js";
48
- /** The real package version β€” read from package.json (at the package root, two up
49
- * from dist/cli/ in the published tarball, and from src/cli/ in dev) so the CLI
50
- * never drifts from what npm shipped. A hardcoded literal here silently lied
51
- * (every release reported 0.1.0 regardless of the actual installed version). */
52
- const HUNCH_VERSION = (() => {
53
- try {
54
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
55
- const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
56
- return typeof v === "string" ? v : "0.0.0";
57
- }
58
- catch {
59
- return "0.0.0";
60
- }
61
- })();
62
48
  const program = new Command();
63
49
  program.name("hunch").description("Hunch β€” an Engineering Memory OS: a git-native reasoning graph for your codebase.").version(HUNCH_VERSION);
64
50
  let openStore = null;
@@ -571,7 +557,7 @@ program
571
557
  if (sources.length > 1)
572
558
  return fail(`pick one of --staged / --commit / --base (got ${sources.join(", ")})`);
573
559
  const markdown = opts.format === "markdown";
574
- const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
560
+ const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], vetoes: [], redundant: [], strictBlockers: 0, regBlocking: 0, vetoBlocking: 0 };
575
561
  const { store, root } = storeFor();
576
562
  // Fail loudly on an unresolvable --base (e.g. CI forgot to fetch the base
577
563
  // branch) β€” otherwise the diff is empty and the guard passes vacuously.
@@ -647,7 +633,7 @@ const vetoCmd = program
647
633
  }
648
634
  // Render ONLY the veto class β€” zero the other sections so the shared renderer
649
635
  // shows just the rejected-alternative reversals (the rest is `hunch check`).
650
- const vetoOnly = { ...full, direct: [], near: [], regressions: [], strictBlockers: 0, regBlocking: 0 };
636
+ const vetoOnly = { ...full, direct: [], near: [], regressions: [], redundant: [], strictBlockers: 0, regBlocking: 0 };
651
637
  console.log(markdown ? renderMarkdown(vetoOnly) : renderText(vetoOnly));
652
638
  if (reportFailsStrict(vetoOnly))
653
639
  process.exitCode = 1;
@@ -4,7 +4,7 @@
4
4
  * then renders it as text (terminal, unchanged) or markdown (a PR comment posted
5
5
  * by the GitHub Action). The exit-code decision lives with the caller. */
6
6
  export function reportIsClean(r) {
7
- return r.direct.length === 0 && r.near.length === 0 && r.regressions.length === 0 && r.vetoes.length === 0;
7
+ return r.direct.length === 0 && r.near.length === 0 && r.regressions.length === 0 && r.vetoes.length === 0 && r.redundant.length === 0;
8
8
  }
9
9
  /** True when --strict should FAIL the commit/PR. */
10
10
  export function reportFailsStrict(r) {
@@ -76,6 +76,12 @@ export function renderText(r) {
76
76
  out.push(` ${v.blocking ? "β›”" : "⚠"} ${v.decision} rejected this approach${v.blocking ? " (human-confirmed)" : " (advisory)"}\n you rejected: ${clip(v.alternative)}\n you chose: ${clip(v.chosen)}\n evidence: ${v.evidence.slice(0, 4).join(", ")}`);
77
77
  }
78
78
  }
79
+ if (r.redundant.length) {
80
+ out.push(`${r.direct.length || r.near.length || r.regressions.length || r.vetoes.length ? "\n" : ""}Possibly re-implements ${r.redundant.length} symbol(s) that already exist (advisory β€” review, never blocks):\n`);
81
+ for (const x of r.redundant) {
82
+ out.push(` ⟲ adds ${x.kind} \`${x.name}\` β€” already defined in ${x.existingFile}`);
83
+ }
84
+ }
79
85
  if (reportFailsStrict(r)) {
80
86
  const reasons = [
81
87
  r.strictBlockers ? `${r.strictBlockers} high-confidence blocking invariant(s) directly in scope` : "",
@@ -142,6 +148,13 @@ export function renderMarkdown(r) {
142
148
  }
143
149
  out.push("");
144
150
  }
151
+ if (r.redundant.length) {
152
+ out.push(`### ⟲ Possibly re-implements existing code (advisory)`);
153
+ for (const x of r.redundant) {
154
+ out.push(`- \`${x.name}\` (${x.kind}) β€” already defined in \`${x.existingFile}\``);
155
+ }
156
+ out.push("");
157
+ }
145
158
  out.push("---");
146
159
  if (reportFailsStrict(r)) {
147
160
  const reasons = [
@@ -0,0 +1,19 @@
1
+ /** The real package version, read once from package.json at the package root β€”
2
+ * two dirs up from dist/<area>/ in the published tarball, and from src/<area>/ in
3
+ * dev. Shared by the CLI (`--version`) and the MCP server (serverInfo) so neither
4
+ * drifts from what npm shipped; a hardcoded literal silently lies (the MCP server
5
+ * reported 0.1.0 for every release until this). Falls back to "0.0.0" if unreadable. */
6
+ import { readFileSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+ export const HUNCH_VERSION = (() => {
10
+ try {
11
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
12
+ const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
13
+ return typeof v === "string" ? v : "0.0.0";
14
+ }
15
+ catch {
16
+ return "0.0.0";
17
+ }
18
+ })();
19
+ //# sourceMappingURL=version.js.map
@@ -17,6 +17,7 @@ import { buildCorrectionConstraint } from "../core/correction.js";
17
17
  import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff } from "../extractors/git.js";
18
18
  import { formatContext } from "../core/format.js";
19
19
  import { renderMarkdown, verdict } from "../core/checkreport.js";
20
+ import { HUNCH_VERSION } from "../core/version.js";
20
21
  const ok = (text) => ({ content: [{ type: "text", text }] });
21
22
  const err = (text) => ({ content: [{ type: "text", text }], isError: true });
22
23
  // Read-side token budgets: every tool result is injected into a Claude Code
@@ -60,7 +61,7 @@ export function buildServer(root) {
60
61
  // the optional model isn't installed). The model then loads lazily on the first
61
62
  // hunch_query and stays warm β€” and hybridSearch degrades to FTS until then.
62
63
  const embedderReady = selectEmbedder();
63
- const server = new McpServer({ name: "hunch", version: "0.1.0" });
64
+ const server = new McpServer({ name: "hunch", version: HUNCH_VERSION });
64
65
  // -- hunch_query ----------------------------------------------------------
65
66
  server.registerTool("hunch_query", {
66
67
  title: "Query Hunch",
@@ -443,6 +443,10 @@ export class HunchStore {
443
443
  const staleIds = new Set(staleRecords.filter((s) => s.kind === "constraint").map((s) => s.id));
444
444
  const staleDecisionIds = new Set(staleRecords.filter((s) => s.kind === "decision").map((s) => s.id));
445
445
  const vetoes = this.vetoHits(an, files, staleDecisionIds);
446
+ const redundant = this.redundantSymbols(an.addedSymbols, files, {
447
+ movedFrom: [...an.filesRenamed.map((r) => r.from), ...an.filesDeleted],
448
+ removedNames: new Set(an.removedSymbols.map((s) => s.name)),
449
+ });
446
450
  const directReport = [...direct.values()].map(({ c, files: fs }) => {
447
451
  const stale = staleIds.has(c.id);
448
452
  const strictBlocks = isStrictBlocker(c, stale);
@@ -460,11 +464,64 @@ export class HunchStore {
460
464
  near: [...near.values()].map(({ c, via }) => ({ id: c.id, severity: c.severity ?? "advisory", statement: c.statement, via })),
461
465
  regressions: regHits.map((h) => ({ kind: h.kind, name: h.name, decision: h.decision, title: h.title, reason: h.reason, blocking: h.blocking })),
462
466
  vetoes: vetoes.map((v) => ({ decision: v.decision, title: v.title, alternative: v.alternative, chosen: v.chosen, tier: v.tier, evidence: v.evidence, blocking: v.blocks })),
467
+ redundant,
463
468
  strictBlockers: directReport.filter((d) => d.strictBlocks).length,
464
469
  regBlocking: regHits.filter((h) => h.blocking).length,
465
470
  vetoBlocking: vetoes.filter((v) => v.blocks).length,
466
471
  };
467
472
  }
473
+ /** Sprawl/"this already exists" guard (ADVISORY, never blocks). A symbol the diff
474
+ * ADDS whose name already exists in the indexed graph in a file NOT touched by the
475
+ * diff is a likely re-implementation the agent's local context window couldn't see.
476
+ * Read-only. Heuristic, so noise is controlled: top-level function/class/const only,
477
+ * name length β‰₯ 4, a stopword list of generic names, deduped, and capped. */
478
+ redundantSymbols(added, files, opts = {}) {
479
+ if (!added.length)
480
+ return [];
481
+ const KINDS = new Set(["function", "class", "const"]);
482
+ const STOP = new Set([
483
+ "index", "handler", "handlers", "default", "main", "run", "start", "stop", "setup", "init",
484
+ "constructor", "render", "create", "update", "build", "parse", "load", "save", "next", "data",
485
+ "value", "item", "items", "props", "state", "config", "options", "result", "route", "routes",
486
+ "app", "server", "client", "test", "tests", "mock", "stub", "helper", "helpers", "util", "utils",
487
+ "types", "schema", "constants", "common", "shared", "base", "model", "models", "view", "store",
488
+ ]);
489
+ // Match only against top-level VALUE declarations already in the graph. A method
490
+ // (`obj.close()`) or the file node itself sharing a name is not a re-implementation.
491
+ // ("variable" is kept for forward-compat; the current indexer emits arrow-fn consts
492
+ // as "function", so const re-implementations are still matched.)
493
+ const EXISTING_KINDS = new Set(["function", "class", "variable"]);
494
+ // A symbol carried into a moved/deleted file is being relocated, not duplicated. The
495
+ // changed-file list uses --diff-filter=ACMR, so a sub-threshold move (Add new + Delete
496
+ // old) drops the old path β€” add the move-from / deleted paths back so their lingering
497
+ // graph entries are not mistaken for a separate "existing" implementation.
498
+ const changed = new Set(files);
499
+ for (const p of opts.movedFrom ?? [])
500
+ changed.add(p);
501
+ const removedNames = opts.removedNames ?? new Set();
502
+ // Only compare within the diff's own top-level root(s). A name that also exists in a
503
+ // test fixture or a separate sub-project (test/, vscode-extension/, site/) is not
504
+ // sprawl in the source under change β€” different roots, different ownership.
505
+ const roots = new Set(files.map((f) => f.split("/")[0]));
506
+ const symbols = this.json.loadAll("symbols");
507
+ const out = [];
508
+ const seen = new Set();
509
+ for (const sc of added) {
510
+ const name = sc.name;
511
+ if (seen.has(name) || name.length < 4 || !KINDS.has(sc.kind) || STOP.has(name.toLowerCase()))
512
+ continue;
513
+ if (removedNames.has(name))
514
+ continue; // the same name was removed in this diff β†’ moved, not duplicated
515
+ const hit = symbols.find((s) => s.name === name && !changed.has(s.file) && EXISTING_KINDS.has(s.kind) && roots.has(s.file.split("/")[0]));
516
+ if (hit) {
517
+ seen.add(name);
518
+ out.push({ name, kind: sc.kind, existingFile: hit.file });
519
+ if (out.length >= 10)
520
+ break;
521
+ }
522
+ }
523
+ return out;
524
+ }
468
525
  /** Time-travel: the decision history for a target β€” every decision touching it,
469
526
  * newest-first, with its valid-time window and supersession links. Answers
470
527
  * "what did we believe, and when/why did it change?" (hunch_timeline). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
6
  "description": "Hunch β€” an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",