@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 +34 -7
- package/dist/cli/index.js +8 -2
- package/dist/extractors/git.js +6 -0
- package/dist/integrations/ciAction.js +19 -6
- package/package.json +68 -68
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/)**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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.
|
|
345
|
-
|
|
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:
|
|
602
|
+
downgrade: c.severity === "blocking" && !strictBlocks ? (stale ? "stale" : "low-confidence") : undefined,
|
|
597
603
|
};
|
|
598
604
|
});
|
|
599
605
|
const report = {
|
package/dist/extractors/git.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
+
}
|