@davesheffer/hunch 0.12.0 → 0.12.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/README.md CHANGED
@@ -149,7 +149,8 @@ preserved) and is idempotent. Opt out with `hunch init --no-providers`.
149
149
  | `hunch embed` | generate local embeddings for semantic recall (opt-in; needs `@huggingface/transformers`) |
150
150
  | `hunch context <path\|symbol> [--as-of <ref>]` | minimal relevant slice for a task: invariants → decisions → bugs → blast radius (`--as-of` time-travels) |
151
151
  | `hunch fragile` | ranked fragility report with evidence |
152
- | `hunch check [--staged\|--commit <sha>] [--strict] [--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); `--blast` prints the dependency fan-out |
152
+ | `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 |
153
+ | `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 |
153
154
  | `hunch stale [--resync]` | drift: records whose files changed after last verification (`--resync` regenerates stale decisions from their commits) |
154
155
  | `hunch review [--accept <id>\|--reject <id>]` | curate: triage / promote / drop low-confidence drafts |
155
156
  | `hunch migrate` | upgrade `.hunch/` records to the current schema version |
@@ -264,6 +265,26 @@ again. It preserves the runner's exit code, so it's a drop-in CI step:
264
265
  Repair drift after refactors with **`hunch stale --resync`** (re-synthesizes stale decisions
265
266
  from their commits via the LLM).
266
267
 
268
+ ### Block a PR that breaks memory (CI Constraint Guard)
269
+
270
+ Memory that only *advises* gets ignored. `hunch ci` scaffolds a GitHub Action that turns
271
+ Hunch into a **merge gate**: on every pull request it runs `hunch check` over the diff,
272
+ posts a sticky comment citing the affected `con_`/`dec_` ids, and **fails the check** when
273
+ the PR breaks a *direct, high-confidence, non-stale* blocking invariant, re-adds
274
+ deliberately-retired code, or contradicts an in-force decision.
275
+
276
+ ```bash
277
+ hunch ci # writes .github/workflows/hunch-guard.yml — commit it
278
+ ```
279
+
280
+ It reasons over **the diff plus the constraints committed in the same git history**, so the
281
+ comment says exactly which decision a change violates ("breaks `con_004` — server-side
282
+ revocation, from `dec_017`"). Make *Hunch Guard* a required status check in branch protection
283
+ to enforce on merge. The hardened strict gate only blocks on high-confidence, non-stale
284
+ invariants — stale / low-confidence / blast-radius hits stay advisory in the comment — so
285
+ it's safe to require on a shared repo. Under the hood it's just
286
+ `hunch check --base origin/<target> --strict --format markdown`.
287
+
267
288
  ## Maintenance
268
289
 
269
290
  - **`hunch doctor`** — is git healthy? are you on the subscription path or the offline
@@ -313,10 +334,15 @@ src/
313
334
 
314
335
  ## VS Code
315
336
 
316
- A companion **[VS Code extension](vscode-extension/)** visualizes Hunch (a tree of
317
- decisions / invariants / bugs / fragility, a "why is this file the way it is?" action, and
318
- a status-bar invariant counter) by reading the committed `.hunch/` JSON directly no
319
- server, no native deps.
337
+ A companion **[VS Code extension](vscode-extension/)** (on
338
+ [Open VSX](https://open-vsx.org/extension/davesheffer/hunch-vscode) works in
339
+ VS Code / Cursor / Windsurf / VSCodium) brings the graph into the editor: a tree of
340
+ decisions / invariants / bugs / **bug-lineage** / fragility / **stale records**, a
341
+ **CodeLens** summary + per-symbol bug/fragility marks, **hover** with bug history,
342
+ invariants surfaced in the **Problems panel**, overview-ruler hotspot marks, an
343
+ interactive **component graph**, fuzzy **search**, and a status-bar invariant counter.
344
+ It reads the committed `.hunch/` JSON directly (no server, no native deps); writes
345
+ delegate to the `hunch` CLI.
320
346
 
321
347
  ## Notable engineering decisions
322
348
 
@@ -341,5 +367,6 @@ npm test # node:test suite (store, graph, parse, indexer, synthesis,
341
367
  npm run hunch -- why src/store/hunchStore.ts # run the CLI from source via tsx, no build
342
368
  ```
343
369
 
344
- See [DESIGN.md](DESIGN.md) for the full spec. Deferred by design: PR/CI webhooks, a
345
- web dashboard, and multi-repo support.
370
+ See [DESIGN.md](DESIGN.md) for the full spec. PR/CI enforcement now ships as the
371
+ **CI Constraint Guard** (`hunch ci`). Still deferred by design: a hosted web dashboard
372
+ and multi-repo support.
package/dist/cli/index.js CHANGED
@@ -24,7 +24,7 @@ import { indexRepo } from "../extractors/indexer.js";
24
24
  import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesize.js";
25
25
  import { parseTestReport } from "../extractors/testreport.js";
26
26
  import { selectProvider } from "../synthesis/provider.js";
27
- import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff } from "../extractors/git.js";
27
+ import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, revExists } from "../extractors/git.js";
28
28
  import { analyzeDiff } from "../extractors/diff.js";
29
29
  import { isStrictBlocker } from "../core/strictgate.js";
30
30
  import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
@@ -550,6 +550,12 @@ program
550
550
  const markdown = opts.format === "markdown";
551
551
  const emptyReport = { fileCount: 0, strict: !!opts.strict, direct: [], near: [], regressions: [], strictBlockers: 0, regBlocking: 0 };
552
552
  const { store, root } = storeFor();
553
+ // Fail loudly on an unresolvable --base (e.g. CI forgot to fetch the base
554
+ // branch) — otherwise the diff is empty and the guard passes vacuously.
555
+ if (opts.base && !revExists(opts.base, root)) {
556
+ store.close();
557
+ return fail(`--base ref "${opts.base}" does not resolve. In CI, fetch the base branch first (git fetch origin <branch>).`);
558
+ }
553
559
  store.reindex(); // blast radius walks the edge graph — make the index current
554
560
  const files = opts.commit ? commitFiles(opts.commit, root)
555
561
  : opts.base ? rangeFiles(opts.base, root)
@@ -593,7 +599,7 @@ program
593
599
  return {
594
600
  id: c.id, severity: c.severity ?? "advisory", statement: c.statement, rationale: c.rationale ?? "",
595
601
  files: fs, strictBlocks,
596
- downgrade: (c.severity === "blocking" && !strictBlocks ? (stale ? "stale" : "low-confidence") : undefined),
602
+ downgrade: c.severity === "blocking" && !strictBlocks ? (stale ? "stale" : "low-confidence") : undefined,
597
603
  };
598
604
  });
599
605
  const report = {
@@ -153,6 +153,12 @@ export function stagedFiles(cwd) {
153
153
  const out = gitSafe(["diff", "--cached", "--name-only", "--diff-filter=ACMR"], cwd);
154
154
  return out ? out.split("\n").filter(Boolean) : [];
155
155
  }
156
+ /** Does a ref resolve to a commit in this repo? Lets `--base` fail LOUDLY on an
157
+ * unfetched/typo'd ref instead of silently diffing against nothing (a vacuous
158
+ * CI pass), since the diff helpers below swallow git errors to "". */
159
+ export function revExists(ref, cwd) {
160
+ return gitSafe(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd) !== "";
161
+ }
156
162
  /** Files a PR/branch changes vs `base` (3-dot: changes on HEAD since the merge-base,
157
163
  * i.e. exactly the PR's own commits — the CI Constraint Guard's surface). */
158
164
  export function rangeFiles(base, cwd, head = "HEAD") {
@@ -38,6 +38,12 @@ jobs:
38
38
  - name: Install Hunch
39
39
  run: npm install -g @davesheffer/hunch
40
40
 
41
+ - name: Fetch the PR base branch
42
+ # checkout sets up no origin/<base> tracking ref; create it explicitly so
43
+ # the guard's base...head diff resolves (otherwise it sees zero changes and
44
+ # passes vacuously).
45
+ run: git fetch --no-tags origin "+refs/heads/\${{ github.base_ref }}:refs/remotes/origin/\${{ github.base_ref }}"
46
+
41
47
  - name: Run Constraint Guard
42
48
  id: guard
43
49
  run: |
@@ -52,19 +58,26 @@ jobs:
52
58
  with:
53
59
  script: |
54
60
  const fs = require('fs');
55
- const body = fs.readFileSync('hunch-report.md', 'utf8').trim();
61
+ const body = (fs.existsSync('hunch-report.md') ? fs.readFileSync('hunch-report.md', 'utf8') : '').trim();
62
+ if (!body) { core.info('Hunch: empty report — skipping comment.'); return; }
56
63
  const marker = '<!-- hunch-guard -->';
57
64
  const { owner, repo } = context.repo;
58
65
  const issue_number = context.payload.pull_request.number;
59
- const all = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
60
- const existing = all.find(c => c.body && c.body.includes(marker));
61
66
  const out = marker + '\\n' + body;
62
- if (existing) await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: out });
63
- else await github.rest.issues.createComment({ owner, repo, issue_number, body: out });
67
+ try {
68
+ const all = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
69
+ const existing = all.find(c => c.body && c.body.includes(marker));
70
+ if (existing) await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: out });
71
+ else await github.rest.issues.createComment({ owner, repo, issue_number, body: out });
72
+ } catch (e) {
73
+ core.warning('Hunch: could not post PR comment (fork PR has a read-only token?): ' + e.message);
74
+ }
64
75
 
65
76
  - name: Enforce (fail on a blocking invariant)
77
+ # Default to 1 if the guard step died before recording its exit — never a
78
+ # vacuous pass.
66
79
  if: always()
67
- run: exit \${{ steps.guard.outputs.exit }}
80
+ run: exit \${{ steps.guard.outputs.exit || '1' }}
68
81
  `;
69
82
  }
70
83
  /** Write .github/workflows/hunch-guard.yml. Never overwrites an existing file
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "@davesheffer/hunch",
3
- "version": "0.12.0",
4
- "license": "MIT",
5
- "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
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-pi.vercel.app",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/davesheffer/hunch.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/davesheffer/hunch/issues"
14
- },
15
- "type": "module",
16
- "bin": {
17
- "hunch": "dist/cli/index.js"
18
- },
19
- "files": [
20
- "dist/**/*.js"
21
- ],
22
- "publishConfig": {
23
- "access": "public"
24
- },
25
- "keywords": [
26
- "claude-code",
27
- "mcp",
28
- "engineering-memory",
29
- "knowledge-graph",
30
- "code-intelligence",
31
- "ai",
32
- "developer-tools"
33
- ],
34
- "engines": {
35
- "node": ">=20"
36
- },
37
- "scripts": {
38
- "clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
39
- "build": "npm run clean && tsc -p tsconfig.json",
40
- "dev": "tsx src/cli/index.ts",
41
- "hunch": "tsx src/cli/index.ts",
42
- "test": "tsx --test test/*.test.ts",
43
- "typecheck": "tsc -p tsconfig.json --noEmit",
44
- "prepublishOnly": "npm run build"
45
- },
46
- "dependencies": {
47
- "@modelcontextprotocol/sdk": "^1.29.0",
48
- "better-sqlite3": "12.9.0",
49
- "commander": "^15.0.0",
50
- "tree-sitter": "0.21.1",
51
- "tree-sitter-typescript": "^0.23.2",
52
- "zod": "^4.4.3"
53
- },
54
- "devDependencies": {
55
- "@types/better-sqlite3": "^7.6.13",
56
- "@types/node": "^20.19.0",
57
- "tsx": "^4.22.4",
58
- "typescript": "^5.9.3"
59
- },
60
- "peerDependencies": {
61
- "@huggingface/transformers": ">=3"
62
- },
63
- "peerDependenciesMeta": {
64
- "@huggingface/transformers": {
65
- "optional": true
66
- }
67
- }
68
- }
1
+ {
2
+ "name": "@davesheffer/hunch",
3
+ "version": "0.12.1",
4
+ "license": "MIT",
5
+ "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
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-pi.vercel.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/davesheffer/hunch.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/davesheffer/hunch/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "hunch": "dist/cli/index.js"
18
+ },
19
+ "files": [
20
+ "dist/**/*.js"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "keywords": [
26
+ "claude-code",
27
+ "mcp",
28
+ "engineering-memory",
29
+ "knowledge-graph",
30
+ "code-intelligence",
31
+ "ai",
32
+ "developer-tools"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
39
+ "build": "npm run clean && tsc -p tsconfig.json",
40
+ "dev": "tsx src/cli/index.ts",
41
+ "hunch": "tsx src/cli/index.ts",
42
+ "test": "tsx --test test/*.test.ts",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "better-sqlite3": "12.9.0",
49
+ "commander": "^15.0.0",
50
+ "tree-sitter": "0.21.1",
51
+ "tree-sitter-typescript": "^0.23.2",
52
+ "zod": "^4.4.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/better-sqlite3": "^7.6.13",
56
+ "@types/node": "^20.19.0",
57
+ "tsx": "^4.22.4",
58
+ "typescript": "^5.9.3"
59
+ },
60
+ "peerDependencies": {
61
+ "@huggingface/transformers": ">=3"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@huggingface/transformers": {
65
+ "optional": true
66
+ }
67
+ }
68
+ }