@davesheffer/hunch 0.12.1 → 0.12.2

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/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { execFileSync, spawnSync } from "node:child_process";
18
18
  import { relative } from "node:path";
19
19
  import { Command } from "commander";
20
- import { hunchPaths, findRoot } from "../core/paths.js";
20
+ import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
21
21
  import { HunchStore } from "../store/hunchStore.js";
22
22
  import { selectEmbedder } from "../store/embedder.js";
23
23
  import { indexRepo } from "../extractors/indexer.js";
@@ -55,7 +55,7 @@ function storeFor() {
55
55
  // ---- init -----------------------------------------------------------------
56
56
  program
57
57
  .command("init")
58
- .description("Scaffold .hunch/, index the repo, install the git hook, and wire up Claude Code.")
58
+ .description("Scaffold .hunch/, index the repo, install the git hook, and wire up your coding assistants (Claude Code, Cursor, VS Code, Windsurf, Codex).")
59
59
  .option("--no-index", "skip the initial repo index")
60
60
  .option("--no-enforce", "do not install the advisory pre-commit constraint guard")
61
61
  .option("--enforce-strict", "make the pre-commit guard FAIL the commit on a direct, high-confidence, non-stale blocking invariant")
@@ -133,7 +133,7 @@ program
133
133
  console.log(` ⚠ skipped ${p.assistant}: ${p.error}`);
134
134
  }
135
135
  store.close();
136
- console.log("\nNext: make a commit (the hook captures a decision), then ask Claude Code \"why is X built this way?\"");
136
+ console.log("\nNext: make a commit (the hook captures a decision), then ask your coding assistant \"why is X built this way?\"");
137
137
  console.log("Cold start? Seed from history: hunch backfill --since 90d");
138
138
  });
139
139
  // ---- index ----------------------------------------------------------------
@@ -409,7 +409,7 @@ program
409
409
  return fail(`--severity must be one of: ${SEV.join(", ")}`);
410
410
  const { store, root } = storeFor();
411
411
  store.json.ensureDirs();
412
- const scope = opts.scope.split(",").map((s) => s.trim()).filter(Boolean);
412
+ const scope = opts.scope.split(",").map((s) => toPosixTarget(s.trim())).filter(Boolean);
413
413
  const c = store.json.put("constraints", {
414
414
  id: constraintId(statement),
415
415
  type: opts.type,
@@ -1,20 +1,37 @@
1
1
  /** Figures out how to re-invoke this CLI from a git hook / .mcp.json, working
2
2
  * both when running the built dist (plain node) and in dev via tsx. */
3
3
  import { fileURLToPath } from "node:url";
4
+ /** Published package name — used for OS-agnostic invocations (see below). */
5
+ const PKG = "@davesheffer/hunch";
4
6
  export function resolveInvocation() {
5
7
  const entry = fileURLToPath(import.meta.url).replace(/invocation\.(js|ts)$/, "index.$1");
6
8
  const isDev = entry.endsWith(".ts");
7
9
  // JSON.stringify yields a double-quoted, backslash-escaped token /bin/sh
8
10
  // accepts — so install paths with spaces don't break the hook command.
9
11
  const q = (s) => JSON.stringify(s);
12
+ // Running from an installed copy (global, local, or npx cache — i.e. NOT a
13
+ // source checkout we're hacking on). The MCP/provider config files we write
14
+ // are committed and shared across a team via git, so they must NOT embed this
15
+ // machine's absolute path or OS-specific separators. Reference Hunch by its
16
+ // published package name instead, which `npx` resolves the same on any OS and
17
+ // any clone. The git hook lives in per-machine .git/hooks (never committed),
18
+ // so it keeps the PATH-robust absolute-node invocation below.
19
+ const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
20
+ if (installed) {
21
+ return {
22
+ shell: `${q(process.execPath)} ${q(entry)}`,
23
+ mcp: { command: "npx", args: ["-y", PKG] },
24
+ };
25
+ }
10
26
  if (isDev) {
11
27
  return {
12
28
  shell: `npx tsx ${q(entry)}`,
13
29
  mcp: { command: "npx", args: ["tsx", entry] },
14
30
  };
15
31
  }
16
- // Use the absolute node binary (process.execPath) rather than a bare `node`,
17
- // so the hook works even when nvm's `node` isn't on the hook's PATH.
32
+ // Source-checkout dist run (e.g. `node dist/cli/index.js`, npm link): inherently
33
+ // per-machine. Use the absolute node binary (process.execPath) rather than a bare
34
+ // `node`, so the hook works even when nvm's `node` isn't on the hook's PATH.
18
35
  return {
19
36
  shell: `${q(process.execPath)} ${q(entry)}`,
20
37
  mcp: { command: process.execPath, args: [entry] },
@@ -3,6 +3,14 @@ import { join } from "node:path";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  export const HUNCH_DIR = ".hunch";
6
+ /** Canonicalize a free-form path/target to repo-relative POSIX form. Hunch stores
7
+ * every path with forward slashes (git emits "/" on all OSes), so any user- or
8
+ * agent-supplied target must be normalized before comparison — otherwise a
9
+ * Windows caller passing `src\auth\session.ts` never matches the stored
10
+ * `src/auth/session.ts`. Safe on symbol names too: they contain no backslashes. */
11
+ export function toPosixTarget(target) {
12
+ return target.replace(/\\/g, "/").replace(/^\.\//, "");
13
+ }
6
14
  export function hunchPaths(root) {
7
15
  const hunch = join(root, HUNCH_DIR);
8
16
  return {
@@ -22,38 +22,38 @@ export function writeMcpJson(root, inv) {
22
22
  writeFileSync(file, JSON.stringify(json, null, 2) + "\n");
23
23
  return file;
24
24
  }
25
- const WHY_CMD = `---
26
- description: Explain why a file or symbol is the way it is, from Hunch
27
- ---
28
- Use the \`hunch_why\` MCP tool on **$ARGUMENTS** (a file path or symbol name).
29
-
30
- Then summarize, with citations:
31
- - the **decisions** that shaped it (id + rationale),
32
- - the **constraints** that must not break,
33
- - the **bug history** behind it (root causes).
34
-
35
- Cite record ids and their provenance/confidence. If Hunch returns nothing,
36
- say so plainly and suggest running \`hunch index\` or \`hunch backfill\`.
25
+ const WHY_CMD = `---
26
+ description: Explain why a file or symbol is the way it is, from Hunch
27
+ ---
28
+ Use the \`hunch_why\` MCP tool on **$ARGUMENTS** (a file path or symbol name).
29
+
30
+ Then summarize, with citations:
31
+ - the **decisions** that shaped it (id + rationale),
32
+ - the **constraints** that must not break,
33
+ - the **bug history** behind it (root causes).
34
+
35
+ Cite record ids and their provenance/confidence. If Hunch returns nothing,
36
+ say so plainly and suggest running \`hunch index\` or \`hunch backfill\`.
37
37
  `;
38
- const FIX_CMD = `---
39
- description: Fix a bug grounded in Hunch (past root causes, constraints, blast radius)
40
- ---
41
- We are fixing: **$ARGUMENTS**
42
-
43
- Follow the Hunch-grounded workflow (DESIGN §5) — do NOT skip the memory lookups:
44
- 1. \`hunch_bug_lineage("$ARGUMENTS")\` — has this class of bug happened before? what was the root cause and the fix?
45
- 2. Identify the suspect symbol/file, then \`hunch_get_dependents(<symbol>)\` to learn the blast radius.
46
- 3. \`hunch_check_constraints(<scope>)\` — list invariants you must preserve.
47
- 4. Propose a fix that honors past root causes AND constraints. Apply it and run the tests.
48
- 5. If the fix encodes a non-trivial choice, \`hunch_record_decision(...)\` so the next session is grounded in it.
38
+ const FIX_CMD = `---
39
+ description: Fix a bug grounded in Hunch (past root causes, constraints, blast radius)
40
+ ---
41
+ We are fixing: **$ARGUMENTS**
42
+
43
+ Follow the Hunch-grounded workflow (DESIGN §5) — do NOT skip the memory lookups:
44
+ 1. \`hunch_bug_lineage("$ARGUMENTS")\` — has this class of bug happened before? what was the root cause and the fix?
45
+ 2. Identify the suspect symbol/file, then \`hunch_get_dependents(<symbol>)\` to learn the blast radius.
46
+ 3. \`hunch_check_constraints(<scope>)\` — list invariants you must preserve.
47
+ 4. Propose a fix that honors past root causes AND constraints. Apply it and run the tests.
48
+ 5. If the fix encodes a non-trivial choice, \`hunch_record_decision(...)\` so the next session is grounded in it.
49
49
  `;
50
- const FRAGILE_CMD = `---
51
- description: Report the most fragile parts of this codebase, with evidence
52
- ---
53
- Ask Hunch for the fragility ranking (run \`hunch fragile\` or query Hunch),
54
- then produce a **fragility report with evidence**: the specific files/functions,
55
- the bug history behind them, their churn and fan-in, and any missing guards.
56
- Avoid generic advice — every claim must cite a Hunch record or metric.
50
+ const FRAGILE_CMD = `---
51
+ description: Report the most fragile parts of this codebase, with evidence
52
+ ---
53
+ Ask Hunch for the fragility ranking (run \`hunch fragile\` or query Hunch),
54
+ then produce a **fragility report with evidence**: the specific files/functions,
55
+ the bug history behind them, their churn and fan-in, and any missing guards.
56
+ Avoid generic advice — every claim must cite a Hunch record or metric.
57
57
  `;
58
58
  /** A settings.json hook entry is Hunch's if any of its commands ends with the
59
59
  * Hunch CLI entry + the `hook` subcommand (e.g. `…/index.js hook`). Matching the
@@ -9,7 +9,7 @@
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
11
  import { z } from "zod";
12
- import { hunchPaths, findRoot } from "../core/paths.js";
12
+ import { hunchPaths, findRoot, toPosixTarget } from "../core/paths.js";
13
13
  import { HunchStore } from "../store/hunchStore.js";
14
14
  import { selectEmbedder } from "../store/embedder.js";
15
15
  import { decisionId } from "../core/ids.js";
@@ -29,6 +29,7 @@ const SEV_BUG = { critical: 4, high: 3, medium: 2, low: 1 };
29
29
  const more = (total, cap, hint = "") => total > cap ? `\n …(+${total - cap} more${hint ? ` — ${hint}` : ""})` : "";
30
30
  /** Resolve a free-form target (symbol id / name / file path) to symbol records. */
31
31
  function resolveSymbols(store, target) {
32
+ target = toPosixTarget(target);
32
33
  const syms = store.json.loadAll("symbols");
33
34
  const byId = syms.find((s) => s.id === target);
34
35
  if (byId)
@@ -42,7 +43,7 @@ function resolveSymbols(store, target) {
42
43
  * radius). Falls back to the literal target so direct-scope checks still run. */
43
44
  function resolveFiles(store, target) {
44
45
  const files = new Set(resolveSymbols(store, target).map((s) => s.file));
45
- return files.size ? [...files] : [target];
46
+ return files.size ? [...files] : [toPosixTarget(target)];
46
47
  }
47
48
  export function buildServer(root) {
48
49
  const store = new HunchStore(hunchPaths(root));
@@ -261,7 +262,7 @@ export function buildServer(root) {
261
262
  consequences: decision.consequences ?? [],
262
263
  alternatives_rejected: decision.alternatives_rejected ?? [],
263
264
  related_components: decision.related_components ?? existing?.related_components ?? [],
264
- related_files: decision.related_files ?? existing?.related_files ?? [],
265
+ related_files: (decision.related_files ?? existing?.related_files ?? []).map(toPosixTarget),
265
266
  supersedes: decision.supersedes ?? existing?.supersedes ?? null,
266
267
  superseded_by: existing?.superseded_by ?? null,
267
268
  caused_by_bug: existing?.caused_by_bug ?? null,
@@ -269,7 +270,7 @@ export function buildServer(root) {
269
270
  valid_from: existing?.valid_from ?? now,
270
271
  valid_to: existing?.valid_to ?? null,
271
272
  retired: existing?.retired ?? { symbols: [], deps: [] },
272
- provenance: { source, confidence: 0.95, evidence: decision.related_files ?? existing?.provenance.evidence ?? [] },
273
+ provenance: { source, confidence: 0.95, evidence: (decision.related_files ?? existing?.provenance.evidence ?? []).map(toPosixTarget) },
273
274
  date: now,
274
275
  };
275
276
  store.json.put("decisions", rec);
@@ -1,3 +1,16 @@
1
+ /**
2
+ * HunchStore — the read/write query layer over the JSON source of truth and the
3
+ * SQLite derived index. Everything the CLI and MCP server need flows through here.
4
+ *
5
+ * - reindex(): JSON -> SQLite (rebuild the derived index + FTS)
6
+ * - search(): FTS5 ranked query (hunch_query)
7
+ * - why(): decisions/bugs/constraints explaining a path/symbol
8
+ * - getDependents(): recursive-CTE blast radius over the call/dep graph
9
+ * - checkConstraints(): constraints whose scope matches a glob/path
10
+ * - bugLineage(): bugs matching a symptom/symbol + their lineage
11
+ * - fragility(): ranked fragility report with evidence
12
+ */
13
+ import { toPosixTarget } from "../core/paths.js";
1
14
  import { ENTITY_KINDS } from "../core/types.js";
2
15
  import { openDb } from "./db.js";
3
16
  import { RESET_SQL, embedHash } from "./schema.js";
@@ -112,7 +125,7 @@ export class HunchStore {
112
125
  if (!match)
113
126
  return this.likeSearch(query, limit);
114
127
  try {
115
- const rows = this.db.prepare(`SELECT ref, kind, title, snippet(search, 3, '[', ']', '…', 12) AS snip, bm25(search) AS score
128
+ const rows = this.db.prepare(`SELECT ref, kind, title, snippet(search, 3, '[', ']', '…', 12) AS snip, bm25(search) AS score
116
129
  FROM search WHERE search MATCH ? ORDER BY score LIMIT ?`).all(match, limit);
117
130
  return rows.map((r) => ({ ref: r.ref, kind: r.kind, title: r.title, snippet: r.snip, score: r.score }));
118
131
  }
@@ -124,7 +137,7 @@ export class HunchStore {
124
137
  /** Substring fallback over titles/bodies (handles non-ASCII / malformed FTS). */
125
138
  likeSearch(query, limit) {
126
139
  const like = `%${query.replace(/[%_]/g, "")}%`;
127
- const rows = this.db.prepare(`SELECT ref, kind, title, substr(body,1,120) AS snip FROM search
140
+ const rows = this.db.prepare(`SELECT ref, kind, title, substr(body,1,120) AS snip FROM search
128
141
  WHERE title LIKE ? OR body LIKE ? LIMIT ?`).all(like, like, limit);
129
142
  return rows.map((r) => ({ ref: r.ref, kind: r.kind, title: r.title, snippet: r.snip, score: 0 }));
130
143
  }
@@ -281,6 +294,7 @@ export class HunchStore {
281
294
  * instant — "what did we believe as of commit X?". Omit `asOf` for the full,
282
295
  * history-inclusive view (backward-compatible default). */
283
296
  why(target, opts = {}) {
297
+ target = toPosixTarget(target);
284
298
  const decisions = this.json.loadAll("decisions");
285
299
  const bugs = this.json.loadAll("bugs");
286
300
  const constraints = this.json.loadAll("constraints");
@@ -308,37 +322,37 @@ export class HunchStore {
308
322
  * walk edges BACKWARD (edges.to = current) following call/dep/import/contains. */
309
323
  getDependents(id, maxDepth = 6) {
310
324
  return this.db.prepare(
311
- /* sql */ `
312
- WITH RECURSIVE up(node, depth) AS (
313
- SELECT ?, 0
314
- UNION
315
- SELECT e."from", up.depth + 1
316
- FROM edges e JOIN up ON e."to" = up.node
317
- WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
318
- )
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
325
+ /* sql */ `
326
+ WITH RECURSIVE up(node, depth) AS (
327
+ SELECT ?, 0
328
+ UNION
329
+ SELECT e."from", up.depth + 1
330
+ FROM edges e JOIN up ON e."to" = up.node
331
+ WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
332
+ )
333
+ SELECT up.node AS id, MIN(up.depth) AS depth,
334
+ COALESCE(s.name || ' @ ' || s.file, c.name, up.node) AS via
335
+ FROM up
336
+ LEFT JOIN symbols s ON s.id = up.node
337
+ LEFT JOIN components c ON c.id = up.node
324
338
  WHERE up.node <> ? GROUP BY up.node ORDER BY depth, id`).all(id, maxDepth, id);
325
339
  }
326
340
  /** Symbols/components this id depends ON (forward walk) — used for refactor blast radius. */
327
341
  getDependencies(id, maxDepth = 6) {
328
342
  return this.db.prepare(
329
- /* sql */ `
330
- WITH RECURSIVE down(node, depth) AS (
331
- SELECT ?, 0
332
- UNION
333
- SELECT e."to", down.depth + 1
334
- FROM edges e JOIN down ON e."from" = down.node
335
- WHERE e.type IN ('calls','depends_on','imports','contains') AND down.depth < ?
336
- )
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
343
+ /* sql */ `
344
+ WITH RECURSIVE down(node, depth) AS (
345
+ SELECT ?, 0
346
+ UNION
347
+ SELECT e."to", down.depth + 1
348
+ FROM edges e JOIN down ON e."from" = down.node
349
+ WHERE e.type IN ('calls','depends_on','imports','contains') AND down.depth < ?
350
+ )
351
+ SELECT down.node AS id, MIN(down.depth) AS depth,
352
+ COALESCE(s.name || ' @ ' || s.file, c.name, down.node) AS via
353
+ FROM down
354
+ LEFT JOIN symbols s ON s.id = down.node
355
+ LEFT JOIN components c ON c.id = down.node
342
356
  WHERE down.node <> ? GROUP BY down.node ORDER BY depth, id`).all(id, maxDepth, id);
343
357
  }
344
358
  /** Files whose symbols (in)directly DEPEND ON a symbol defined in `file` — the
@@ -350,17 +364,17 @@ export class HunchStore {
350
364
  // the bare `via` take the name from the nearest-depth row. The inner JOIN drops
351
365
  // non-symbol nodes; `s.file <> ?` drops self-file dependents.
352
366
  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 <> ?
367
+ /* sql */ `
368
+ WITH RECURSIVE up(node, depth) AS (
369
+ SELECT id, 0 FROM symbols WHERE file = ?
370
+ UNION
371
+ SELECT e."from", up.depth + 1
372
+ FROM edges e JOIN up ON e."to" = up.node
373
+ WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
374
+ )
375
+ SELECT s.file AS file, s.name AS via, MIN(up.depth) AS depth
376
+ FROM up JOIN symbols s ON s.id = up.node
377
+ WHERE up.depth > 0 AND s.file <> ?
364
378
  GROUP BY s.file ORDER BY depth, file`).all(file, maxDepth, file);
365
379
  }
366
380
  /** Constraints whose scope glob matches a path/glob (hunch_check_constraints).
@@ -558,6 +572,7 @@ export class HunchStore {
558
572
  * a task on `target`, ordered by what matters most — invariants first, then the
559
573
  * why, then blast radius and bug history — trimmed to a rough token budget. */
560
574
  assembleContext(target, budget = 1500, opts = {}) {
575
+ target = toPosixTarget(target);
561
576
  const w = this.why(target, opts);
562
577
  const symIds = w.symbols.map((s) => s.id);
563
578
  const blast = new Map();
@@ -15,87 +15,87 @@ import { shortHash } from "../core/ids.js";
15
15
  export function embedHash(title, body) {
16
16
  return shortHash(`${title}\x00${body ?? ""}`, 16);
17
17
  }
18
- export const SCHEMA_SQL = /* sql */ `
19
- PRAGMA journal_mode = WAL;
20
- PRAGMA foreign_keys = OFF;
21
-
22
- CREATE TABLE IF NOT EXISTS components (
23
- id TEXT PRIMARY KEY,
24
- kind TEXT, name TEXT, responsibility TEXT,
25
- paths TEXT, status TEXT, owners TEXT,
26
- fragility REAL,
27
- prov_source TEXT, prov_confidence REAL, prov_evidence TEXT,
28
- created_at TEXT, updated_at TEXT
29
- );
30
-
31
- CREATE TABLE IF NOT EXISTS edges (
32
- id TEXT PRIMARY KEY,
33
- "from" TEXT, "to" TEXT, type TEXT, reason TEXT, strength REAL,
34
- prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
35
- );
36
- CREATE INDEX IF NOT EXISTS idx_edges_from ON edges("from");
37
- CREATE INDEX IF NOT EXISTS idx_edges_to ON edges("to");
38
- CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
39
-
40
- CREATE TABLE IF NOT EXISTS symbols (
41
- id TEXT PRIMARY KEY,
42
- file TEXT, name TEXT, kind TEXT, signature_hash TEXT,
43
- calls TEXT, called_by TEXT,
44
- loc INTEGER, churn_90d INTEGER, bug_count INTEGER, fan_in INTEGER, fan_out INTEGER,
45
- last_changed TEXT
46
- );
47
- CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file);
48
- CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
49
-
50
- CREATE TABLE IF NOT EXISTS decisions (
51
- id TEXT PRIMARY KEY,
52
- title TEXT, status TEXT, context TEXT, decision TEXT,
53
- consequences TEXT, alternatives_rejected TEXT,
54
- related_components TEXT, related_files TEXT,
55
- supersedes TEXT, caused_by_bug TEXT, "commit" TEXT,
56
- prov_source TEXT, prov_confidence REAL, prov_evidence TEXT,
57
- date TEXT
58
- );
59
-
60
- CREATE TABLE IF NOT EXISTS bugs (
61
- id TEXT PRIMARY KEY,
62
- title TEXT, symptom TEXT, root_cause TEXT, severity TEXT, status TEXT,
63
- affected_files TEXT, affected_symbols TEXT, lineage TEXT,
64
- prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
65
- );
66
-
67
- CREATE TABLE IF NOT EXISTS constraints (
68
- id TEXT PRIMARY KEY,
69
- type TEXT, statement TEXT, scope TEXT, severity TEXT, enforcement TEXT,
70
- rationale TEXT, source_decision TEXT, violations TEXT,
71
- prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
72
- );
73
-
74
- -- Unified full-text search across every entity. Rebuilt on index; bm25-ranked.
75
- CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(
76
- ref UNINDEXED, -- entity id
77
- kind UNINDEXED, -- components | edges | symbols | decisions | bugs | constraints
78
- title,
79
- body,
80
- tokenize = 'porter unicode61'
81
- );
82
-
83
- -- Local semantic-search vectors (opt-in; written by \`hunch embed\`). One row per
84
- -- (ref, model); vec is a Float32 BLOB. DELIBERATELY NOT in RESET_SQL: reindex()
85
- -- runs RESET on nearly every path (MCP startup, every query/context), so resetting
86
- -- embeddings here would wipe them constantly and make the feature a no-op. Staleness
87
- -- is tracked by doc_hash and reconciled by pruneStaleEmbeddings() instead. Recall is
88
- -- exact brute-force cosine in JS (graphs are small); sqlite-vec only past ~100k rows.
89
- CREATE TABLE IF NOT EXISTS embeddings (
90
- ref TEXT, kind TEXT, model TEXT, dim INTEGER, doc_hash TEXT, vec BLOB,
91
- PRIMARY KEY (ref, model)
92
- );
18
+ export const SCHEMA_SQL = /* sql */ `
19
+ PRAGMA journal_mode = WAL;
20
+ PRAGMA foreign_keys = OFF;
21
+
22
+ CREATE TABLE IF NOT EXISTS components (
23
+ id TEXT PRIMARY KEY,
24
+ kind TEXT, name TEXT, responsibility TEXT,
25
+ paths TEXT, status TEXT, owners TEXT,
26
+ fragility REAL,
27
+ prov_source TEXT, prov_confidence REAL, prov_evidence TEXT,
28
+ created_at TEXT, updated_at TEXT
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS edges (
32
+ id TEXT PRIMARY KEY,
33
+ "from" TEXT, "to" TEXT, type TEXT, reason TEXT, strength REAL,
34
+ prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
35
+ );
36
+ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges("from");
37
+ CREATE INDEX IF NOT EXISTS idx_edges_to ON edges("to");
38
+ CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
39
+
40
+ CREATE TABLE IF NOT EXISTS symbols (
41
+ id TEXT PRIMARY KEY,
42
+ file TEXT, name TEXT, kind TEXT, signature_hash TEXT,
43
+ calls TEXT, called_by TEXT,
44
+ loc INTEGER, churn_90d INTEGER, bug_count INTEGER, fan_in INTEGER, fan_out INTEGER,
45
+ last_changed TEXT
46
+ );
47
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file);
48
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
49
+
50
+ CREATE TABLE IF NOT EXISTS decisions (
51
+ id TEXT PRIMARY KEY,
52
+ title TEXT, status TEXT, context TEXT, decision TEXT,
53
+ consequences TEXT, alternatives_rejected TEXT,
54
+ related_components TEXT, related_files TEXT,
55
+ supersedes TEXT, caused_by_bug TEXT, "commit" TEXT,
56
+ prov_source TEXT, prov_confidence REAL, prov_evidence TEXT,
57
+ date TEXT
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS bugs (
61
+ id TEXT PRIMARY KEY,
62
+ title TEXT, symptom TEXT, root_cause TEXT, severity TEXT, status TEXT,
63
+ affected_files TEXT, affected_symbols TEXT, lineage TEXT,
64
+ prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS constraints (
68
+ id TEXT PRIMARY KEY,
69
+ type TEXT, statement TEXT, scope TEXT, severity TEXT, enforcement TEXT,
70
+ rationale TEXT, source_decision TEXT, violations TEXT,
71
+ prov_source TEXT, prov_confidence REAL, prov_evidence TEXT
72
+ );
73
+
74
+ -- Unified full-text search across every entity. Rebuilt on index; bm25-ranked.
75
+ CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(
76
+ ref UNINDEXED, -- entity id
77
+ kind UNINDEXED, -- components | edges | symbols | decisions | bugs | constraints
78
+ title,
79
+ body,
80
+ tokenize = 'porter unicode61'
81
+ );
82
+
83
+ -- Local semantic-search vectors (opt-in; written by \`hunch embed\`). One row per
84
+ -- (ref, model); vec is a Float32 BLOB. DELIBERATELY NOT in RESET_SQL: reindex()
85
+ -- runs RESET on nearly every path (MCP startup, every query/context), so resetting
86
+ -- embeddings here would wipe them constantly and make the feature a no-op. Staleness
87
+ -- is tracked by doc_hash and reconciled by pruneStaleEmbeddings() instead. Recall is
88
+ -- exact brute-force cosine in JS (graphs are small); sqlite-vec only past ~100k rows.
89
+ CREATE TABLE IF NOT EXISTS embeddings (
90
+ ref TEXT, kind TEXT, model TEXT, dim INTEGER, doc_hash TEXT, vec BLOB,
91
+ PRIMARY KEY (ref, model)
92
+ );
93
93
  `;
94
94
  /** Drop derived data (used before a full reindex). NOTE: embeddings is omitted on
95
95
  * purpose — see the embeddings table comment above. */
96
- export const RESET_SQL = /* sql */ `
97
- DELETE FROM components; DELETE FROM edges; DELETE FROM symbols;
98
- DELETE FROM decisions; DELETE FROM bugs; DELETE FROM constraints;
99
- DELETE FROM search;
96
+ export const RESET_SQL = /* sql */ `
97
+ DELETE FROM components; DELETE FROM edges; DELETE FROM symbols;
98
+ DELETE FROM decisions; DELETE FROM bugs; DELETE FROM constraints;
99
+ DELETE FROM search;
100
100
  `;
101
101
  //# sourceMappingURL=schema.js.map
@@ -96,10 +96,10 @@ export function pexecIn(cmd, args, opts = {}) {
96
96
  child.stdin.end();
97
97
  });
98
98
  }
99
- const SYSTEM = `You are the synthesis engine of an Engineering Memory OS. You turn raw
100
- developer activity (a git commit diff, or a test failure) into a single structured
101
- "why" record. Be precise and evidence-grounded; never invent facts not supported by
102
- the input. Prefer short, concrete statements. If intent is unclear, say so plainly
99
+ const SYSTEM = `You are the synthesis engine of an Engineering Memory OS. You turn raw
100
+ developer activity (a git commit diff, or a test failure) into a single structured
101
+ "why" record. Be precise and evidence-grounded; never invent facts not supported by
102
+ the input. Prefer short, concrete statements. If intent is unclear, say so plainly
103
103
  rather than guessing.`;
104
104
  const DECISION_TOOL = {
105
105
  name: "emit_decision",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
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.",