@davesheffer/hunch 0.9.0 → 0.10.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Dave Sheffer
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dave Sheffer
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
@@ -124,6 +124,7 @@ they all consult the same memory:
124
124
  | Cursor | `.cursor/mcp.json` | `.cursor/rules/hunch.mdc` (always-applied) |
125
125
  | VS Code (Copilot) | `.vscode/mcp.json` | `.github/copilot-instructions.md` |
126
126
  | Codex CLI | `.codex/config.toml` | `AGENTS.md` |
127
+ | Windsurf | `.windsurf/mcp_config.json` | `.windsurf/rules/hunch.md` (always-on) |
127
128
  | Anything else | — | `AGENTS.md` (cross-tool standard) |
128
129
 
129
130
  Each writer **merges** into existing files (other MCP servers and your own prose are
@@ -133,7 +134,7 @@ preserved) and is idempotent. Opt out with `hunch init --no-providers`.
133
134
 
134
135
  | Command | What |
135
136
  |---|---|
136
- | `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, AGENTS.md). Flags: `--no-enforce`, `--enforce-strict`, `--no-providers`, `--no-agent-hooks`, `--firmness <level>` |
137
+ | `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>` |
137
138
  | `hunch index` | parse repo → symbols / edges / components (deterministic, no LLM) |
138
139
  | `hunch backfill --since 90d` | replay git history → seed decisions |
139
140
  | `hunch sync [sha]` | turn a commit into a Decision (run automatically by the hook) |
@@ -26,19 +26,19 @@ const BUILTIN_METHODS = new Set([
26
26
  "log", "error", "warn", "info", "debug",
27
27
  ]);
28
28
  /** Tree-sitter query capturing every construct we care about in one pass. */
29
- const QUERY_SRC = `
30
- (function_declaration name: (identifier) @fn.name) @fn.def
31
- (generator_function_declaration name: (identifier) @fn.name) @fn.def
32
- (method_definition name: (property_identifier) @method.name) @method.def
33
- (class_declaration name: (type_identifier) @class.name) @class.def
34
- (interface_declaration name: (type_identifier) @iface.name) @iface.def
35
- (type_alias_declaration name: (type_identifier) @type.name) @type.def
36
- (variable_declarator
37
- name: (identifier) @arrow.name
38
- value: [(arrow_function) (function_expression)]) @arrow.def
39
- (import_statement source: (string) @import.src)
40
- (call_expression function: (identifier) @call.id)
41
- (call_expression function: (member_expression property: (property_identifier) @call.member))
29
+ const QUERY_SRC = `
30
+ (function_declaration name: (identifier) @fn.name) @fn.def
31
+ (generator_function_declaration name: (identifier) @fn.name) @fn.def
32
+ (method_definition name: (property_identifier) @method.name) @method.def
33
+ (class_declaration name: (type_identifier) @class.name) @class.def
34
+ (interface_declaration name: (type_identifier) @iface.name) @iface.def
35
+ (type_alias_declaration name: (type_identifier) @type.name) @type.def
36
+ (variable_declarator
37
+ name: (identifier) @arrow.name
38
+ value: [(arrow_function) (function_expression)]) @arrow.def
39
+ (import_statement source: (string) @import.src)
40
+ (call_expression function: (identifier) @call.id)
41
+ (call_expression function: (member_expression property: (property_identifier) @call.member))
42
42
  `;
43
43
  const cache = new Map();
44
44
  function bundleFor(lang, key) {
@@ -11,6 +11,7 @@
11
11
  * Cursor | .cursor/mcp.json | mcpServers | .cursor/rules/hunch.mdc
12
12
  * VS Code | .vscode/mcp.json | servers (+type)| .github/copilot-instructions.md
13
13
  * Codex CLI | .codex/config.toml | [mcp_servers.*]| AGENTS.md
14
+ * Windsurf | .windsurf/mcp_config.json| mcpServers | .windsurf/rules/hunch.md
14
15
  * (any other) | — | — | AGENTS.md (cross-tool standard)
15
16
  *
16
17
  * Every writer MERGES into existing files (preserving other servers / user prose)
@@ -206,6 +207,25 @@ export function writeCursorRule(root, store) {
206
207
  writeFileSync(file, body);
207
208
  return file;
208
209
  }
210
+ /** Windsurf (Cascade): .windsurf/mcp_config.json — same `mcpServers` shape as
211
+ * Cursor. Repo-local (committed, shared via git) to match Hunch's other configs,
212
+ * rather than the global ~/.codeium/windsurf path. Merges; refuses to clobber. */
213
+ export function writeWindsurfMcp(root, inv) {
214
+ const file = join(root, ".windsurf", "mcp_config.json");
215
+ const json = readJsonObj(file);
216
+ json.mcpServers = json.mcpServers ?? {};
217
+ json.mcpServers.hunch = { command: inv.command, args: [...inv.args, "mcp"] };
218
+ return writeJson(file, json);
219
+ }
220
+ /** Windsurf project rule (.windsurf/rules/hunch.md). `trigger: always_on` keeps the
221
+ * Hunch grounding in Cascade's context for every request. Fully managed (overwritten). */
222
+ export function writeWindsurfRule(root, store) {
223
+ const file = join(root, ".windsurf", "rules", "hunch.md");
224
+ const body = `---\ntrigger: always_on\ndescription: Hunch engineering memory — consult the hunch_* MCP tools before editing\n---\n\n${renderHunchSection(store)}\n`;
225
+ mkdirSync(dirname(file), { recursive: true });
226
+ writeFileSync(file, body);
227
+ return file;
228
+ }
209
229
  /** Scaffold MCP config + grounding for all supported assistants. Returns a
210
230
  * per-assistant summary for `hunch init` to print. Each assistant is isolated:
211
231
  * a writer that refuses to clobber a malformed file degrades to a warning rather
@@ -215,6 +235,7 @@ export function scaffoldProviders(root, inv, store) {
215
235
  ["Cursor", () => [writeCursorMcp(root, inv), writeCursorRule(root, store)]],
216
236
  ["VS Code (Copilot)", () => [writeVscodeMcp(root, inv), writeCopilotInstructions(root, store)]],
217
237
  ["Codex CLI", () => [writeCodexConfig(root, inv)]],
238
+ ["Windsurf", () => [writeWindsurfMcp(root, inv), writeWindsurfRule(root, store)]],
218
239
  ["Any (AGENTS.md)", () => [writeAgentsMd(root, store)]],
219
240
  ];
220
241
  return tasks.map(([assistant, run]) => {
@@ -30,6 +30,11 @@ export class HunchStore {
30
30
  const tx = db.transaction(() => {
31
31
  db.exec(RESET_SQL);
32
32
  const j = (s) => s; // readability marker for JSON-encoded columns
33
+ // Prepare the FTS insert ONCE (after RESET created the table), not per row.
34
+ const insFts = db.prepare(`INSERT INTO search (ref, kind, title, body) VALUES (?,?,?,?)`);
35
+ const fts = (ref, kind, title, body) => {
36
+ insFts.run(ref, kind, title, body ?? "");
37
+ };
33
38
  const comps = this.json.loadAll("components");
34
39
  const insComp = db.prepare(`INSERT INTO components VALUES (@id,@kind,@name,@responsibility,@paths,@status,@owners,@fragility,@ps,@pc,@pe,@created_at,@updated_at)`);
35
40
  for (const c of comps) {
@@ -39,7 +44,7 @@ export class HunchStore {
39
44
  fragility: c.fragility, ps: c.provenance.source, pc: c.provenance.confidence,
40
45
  pe: JSON.stringify(c.provenance.evidence), created_at: c.created_at, updated_at: c.updated_at,
41
46
  });
42
- this.fts(db, c.id, "components", c.name, `${c.responsibility} ${c.paths.join(" ")}`);
47
+ fts(c.id, "components", c.name, `${c.responsibility} ${c.paths.join(" ")}`);
43
48
  }
44
49
  counts.components = comps.length;
45
50
  const edges = this.json.loadAll("edges");
@@ -56,7 +61,7 @@ export class HunchStore {
56
61
  calls: JSON.stringify(s.calls), called_by: JSON.stringify(s.called_by),
57
62
  loc: s.metrics.loc, churn: s.metrics.churn_90d, bug: s.metrics.bug_count,
58
63
  fanin: s.metrics.fan_in, fanout: s.metrics.fan_out, last: s.last_changed });
59
- this.fts(db, s.id, "symbols", `${s.name} (${s.kind})`, s.file);
64
+ fts(s.id, "symbols", `${s.name} (${s.kind})`, s.file);
60
65
  }
61
66
  counts.symbols = syms.length;
62
67
  const decs = this.json.loadAll("decisions");
@@ -67,7 +72,7 @@ export class HunchStore {
67
72
  rc: JSON.stringify(d.related_components), rf: JSON.stringify(d.related_files),
68
73
  sup: d.supersedes, cbb: d.caused_by_bug, commit: d.commit,
69
74
  ps: d.provenance.source, pc: d.provenance.confidence, pe: JSON.stringify(d.provenance.evidence), date: d.date });
70
- this.fts(db, d.id, "decisions", d.title, `${d.context} ${d.decision} ${d.consequences.join(" ")}`);
75
+ fts(d.id, "decisions", d.title, `${d.context} ${d.decision} ${d.consequences.join(" ")}`);
71
76
  }
72
77
  counts.decisions = decs.length;
73
78
  const bugs = this.json.loadAll("bugs");
@@ -76,7 +81,7 @@ export class HunchStore {
76
81
  insBug.run({ id: b.id, title: b.title, symptom: b.symptom, rc: b.root_cause, sev: b.severity, status: b.status,
77
82
  af: JSON.stringify(b.affected_files), as: JSON.stringify(b.affected_symbols), lin: JSON.stringify(b.lineage),
78
83
  ps: b.provenance.source, pc: b.provenance.confidence, pe: JSON.stringify(b.provenance.evidence) });
79
- this.fts(db, b.id, "bugs", b.title, `${b.symptom} ${b.root_cause}`);
84
+ fts(b.id, "bugs", b.title, `${b.symptom} ${b.root_cause}`);
80
85
  }
81
86
  counts.bugs = bugs.length;
82
87
  const cons = this.json.loadAll("constraints");
@@ -85,7 +90,7 @@ export class HunchStore {
85
90
  insCon.run({ id: c.id, type: c.type, statement: c.statement, scope: JSON.stringify(c.scope),
86
91
  sev: c.severity, enf: c.enforcement, rat: c.rationale, sd: c.source_decision, viol: JSON.stringify(c.violations),
87
92
  ps: c.provenance.source, pc: c.provenance.confidence, pe: JSON.stringify(c.provenance.evidence) });
88
- this.fts(db, c.id, "constraints", c.statement, `${c.rationale} ${c.scope.join(" ")}`);
93
+ fts(c.id, "constraints", c.statement, `${c.rationale} ${c.scope.join(" ")}`);
89
94
  }
90
95
  counts.constraints = cons.length;
91
96
  void j;
@@ -97,9 +102,6 @@ export class HunchStore {
97
102
  this.pruneStaleEmbeddings();
98
103
  return { counts };
99
104
  }
100
- fts(db, ref, kind, title, body) {
101
- db.prepare(`INSERT INTO search (ref, kind, title, body) VALUES (?,?,?,?)`).run(ref, kind, title, body ?? "");
102
- }
103
105
  // ---- read path ----------------------------------------------------------
104
106
  /** FTS5 ranked search (hunch_query). Falls back to LIKE if the query has no
105
107
  * FTS-tokenizable terms. */
@@ -305,7 +307,7 @@ export class HunchStore {
305
307
  * `id`, via a recursive CTE over the edges graph (hunch_get_dependents). We
306
308
  * walk edges BACKWARD (edges.to = current) following call/dep/import/contains. */
307
309
  getDependents(id, maxDepth = 6) {
308
- const rows = this.db.prepare(
310
+ return this.db.prepare(
309
311
  /* sql */ `
310
312
  WITH RECURSIVE up(node, depth) AS (
311
313
  SELECT ?, 0
@@ -314,13 +316,16 @@ export class HunchStore {
314
316
  FROM edges e JOIN up ON e."to" = up.node
315
317
  WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
316
318
  )
317
- SELECT DISTINCT up.node AS id, MIN(up.depth) AS depth FROM up
319
+ SELECT up.node AS id, MIN(up.depth) AS depth,
320
+ COALESCE(s.name || ' @ ' || s.file, c.name, up.node) AS via
321
+ FROM up
322
+ LEFT JOIN symbols s ON s.id = up.node
323
+ LEFT JOIN components c ON c.id = up.node
318
324
  WHERE up.node <> ? GROUP BY up.node ORDER BY depth, id`).all(id, maxDepth, id);
319
- return rows.map((r) => ({ id: r.id, depth: r.depth, via: this.labelFor(r.id) }));
320
325
  }
321
326
  /** Symbols/components this id depends ON (forward walk) — used for refactor blast radius. */
322
327
  getDependencies(id, maxDepth = 6) {
323
- const rows = this.db.prepare(
328
+ return this.db.prepare(
324
329
  /* sql */ `
325
330
  WITH RECURSIVE down(node, depth) AS (
326
331
  SELECT ?, 0
@@ -329,40 +334,34 @@ export class HunchStore {
329
334
  FROM edges e JOIN down ON e."from" = down.node
330
335
  WHERE e.type IN ('calls','depends_on','imports','contains') AND down.depth < ?
331
336
  )
332
- SELECT DISTINCT down.node AS id, MIN(down.depth) AS depth FROM down
337
+ SELECT down.node AS id, MIN(down.depth) AS depth,
338
+ COALESCE(s.name || ' @ ' || s.file, c.name, down.node) AS via
339
+ FROM down
340
+ LEFT JOIN symbols s ON s.id = down.node
341
+ LEFT JOIN components c ON c.id = down.node
333
342
  WHERE down.node <> ? GROUP BY down.node ORDER BY depth, id`).all(id, maxDepth, id);
334
- return rows.map((r) => ({ id: r.id, depth: r.depth, via: this.labelFor(r.id) }));
335
- }
336
- labelFor(id) {
337
- if (id.startsWith("sym_")) {
338
- const r = this.db.prepare(`SELECT name, file FROM symbols WHERE id=?`).get(id);
339
- return r ? `${r.name} @ ${r.file}` : id;
340
- }
341
- if (id.startsWith("cmp_")) {
342
- const r = this.db.prepare(`SELECT name FROM components WHERE id=?`).get(id);
343
- return r ? r.name : id;
344
- }
345
- return id;
346
343
  }
347
344
  /** Files whose symbols (in)directly DEPEND ON a symbol defined in `file` — the
348
345
  * blast radius of editing `file`, collapsed to file granularity (nearest depth
349
346
  * wins per file). Powers `hunch check` near-violation detection and `--blast`. */
350
347
  blastRadiusFiles(file, maxDepth = 4) {
351
- const syms = this.db.prepare(`SELECT id FROM symbols WHERE file = ?`).all(file);
352
- const out = new Map();
353
- for (const s of syms) {
354
- for (const dep of this.getDependents(s.id, maxDepth)) {
355
- if (!dep.id.startsWith("sym_"))
356
- continue;
357
- const r = this.db.prepare(`SELECT name, file FROM symbols WHERE id=?`).get(dep.id);
358
- if (!r || r.file === file)
359
- continue; // ignore self-file dependents
360
- const prev = out.get(r.file);
361
- if (!prev || dep.depth < prev.depth)
362
- out.set(r.file, { file: r.file, via: r.name, depth: dep.depth });
363
- }
364
- }
365
- return [...out.values()].sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
348
+ // ONE backward CTE seeded by every symbol in `file`, joined to symbols for the
349
+ // dependent file/name. GROUP BY file + MIN(depth): SQLite's single-min rule makes
350
+ // the bare `via` take the name from the nearest-depth row. The inner JOIN drops
351
+ // non-symbol nodes; `s.file <> ?` drops self-file dependents.
352
+ return this.db.prepare(
353
+ /* sql */ `
354
+ WITH RECURSIVE up(node, depth) AS (
355
+ SELECT id, 0 FROM symbols WHERE file = ?
356
+ UNION
357
+ SELECT e."from", up.depth + 1
358
+ FROM edges e JOIN up ON e."to" = up.node
359
+ WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
360
+ )
361
+ SELECT s.file AS file, s.name AS via, MIN(up.depth) AS depth
362
+ FROM up JOIN symbols s ON s.id = up.node
363
+ WHERE up.depth > 0 AND s.file <> ?
364
+ GROUP BY s.file ORDER BY depth, file`).all(file, maxDepth, file);
366
365
  }
367
366
  /** Constraints whose scope glob matches a path/glob (hunch_check_constraints).
368
367
  * By default only ACTIVE invariants are returned — a retired constraint is no
@@ -3,7 +3,7 @@
3
3
  * in PRs, diffable, mergeable). This layer never touches SQLite — it is the
4
4
  * authoritative read/write surface; SQLite is rebuilt from it.
5
5
  */
6
- import { mkdirSync, readdirSync, readFileSync, existsSync, rmSync } from "node:fs";
6
+ import { mkdirSync, readdirSync, readFileSync, existsSync, rmSync, statSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { ENTITY_KINDS, SCHEMAS } from "../core/types.js";
9
9
  import { migrateRaw, readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js";
@@ -17,9 +17,27 @@ const encode = (v) => JSON.stringify(v, null, 2) + "\n";
17
17
  export class JsonStore {
18
18
  paths;
19
19
  _warnedForward = false;
20
+ /** Memoized validated records per kind. loadAll() is on every read-path method
21
+ * (why/checkConstraints/fragility/assembleContext…), each of which previously
22
+ * re-read the directory and re-ran Zod over every record. Cache keyed by the
23
+ * kind dir's mtime: every write goes through writeFileAtomic (temp + rename) or
24
+ * rmSync, both of which bump the dir mtime — so an OUT-OF-BAND write by another
25
+ * process self-invalidates on the next read. In-process writes also invalidate
26
+ * explicitly (exact, independent of mtime granularity). */
27
+ cache = new Map();
20
28
  constructor(paths) {
21
29
  this.paths = paths;
22
30
  }
31
+ /** Drop memoized loadAll results. Writes through this store invalidate the
32
+ * affected kind automatically; call this for OUT-OF-BAND changes to .hunch/
33
+ * JSON (e.g. the long-lived MCP server reacting to a file-watch, or after an
34
+ * external `hunch migrate`). */
35
+ clearCache() {
36
+ this.cache.clear();
37
+ }
38
+ invalidate(kind) {
39
+ this.cache.delete(kind);
40
+ }
23
41
  /** Create .hunch/<kind>/ directories. Stamp the manifest at the CURRENT version
24
42
  * only when scaffolding a FRESH .hunch/ (so `hunch index`/`sync` on a brand-new
25
43
  * repo records the version too, not just `init`). A pre-existing .hunch/ without
@@ -56,9 +74,22 @@ export class JsonStore {
56
74
  return join(this.paths.dir(kind), single);
57
75
  return join(this.paths.dir(kind), `${id}.json`);
58
76
  }
59
- /** Load every record of a kind, validated against its schema. Invalid records
60
- * are skipped with a warning rather than crashing the whole load. */
77
+ /** Load every record of a kind, validated against its schema. Memoized — the
78
+ * returned array is shared and MUST be treated read-only (every caller already
79
+ * derives via filter/map/sort, which copy). Invalidated on write. */
61
80
  loadAll(kind) {
81
+ const dir = this.paths.dir(kind);
82
+ const mtimeMs = existsSync(dir) ? statSync(dir).mtimeMs : 0;
83
+ const hit = this.cache.get(kind);
84
+ if (hit && hit.mtimeMs === mtimeMs)
85
+ return hit.data;
86
+ const data = this.readAllFromDisk(kind);
87
+ this.cache.set(kind, { mtimeMs, data });
88
+ return data;
89
+ }
90
+ /** Uncached disk read + validate. Invalid records are skipped with a warning
91
+ * rather than crashing the whole load. */
92
+ readAllFromDisk(kind) {
62
93
  const dir = this.paths.dir(kind);
63
94
  if (!existsSync(dir))
64
95
  return [];
@@ -126,12 +157,14 @@ export class JsonStore {
126
157
  else {
127
158
  writeFileAtomic(this.fileFor(kind, validated.id), encode(validated));
128
159
  }
160
+ this.invalidate(kind);
129
161
  return validated;
130
162
  }
131
163
  /** Bulk replace all records of a kind (used by the extractor for symbols/edges). */
132
164
  replaceAll(kind, records) {
133
165
  const schema = SCHEMAS[kind];
134
166
  const validated = records.map((r) => schema.parse(r));
167
+ this.invalidate(kind);
135
168
  mkdirSync(this.paths.dir(kind), { recursive: true });
136
169
  const single = SINGLE_FILE[kind];
137
170
  if (single) {
@@ -186,12 +219,14 @@ export class JsonStore {
186
219
  if (next.length === arr.length)
187
220
  return false;
188
221
  writeFileAtomic(f, encode(next));
222
+ this.invalidate(kind);
189
223
  return true;
190
224
  }
191
225
  const f = this.fileFor(kind, id);
192
226
  if (!existsSync(f))
193
227
  return false;
194
228
  rmSync(f);
229
+ this.invalidate(kind);
195
230
  return true;
196
231
  }
197
232
  /** Persist a schema migration: rewrite every LOADABLE record in its current shape.
@@ -201,6 +236,7 @@ export class JsonStore {
201
236
  persistMigration() {
202
237
  let migrated = 0;
203
238
  let skipped = 0;
239
+ this.cache.clear(); // every record is rewritten; drop all memoized loads
204
240
  const version = this.schemaVersion(); // read once; we're migrating FROM this
205
241
  for (const kind of ENTITY_KINDS) {
206
242
  const dir = this.paths.dir(kind);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
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.",
7
- "homepage": "https://hunch.sh",
7
+ "homepage": "https://hunch-pi.vercel.app",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/davesheffer/hunch.git"