@davesheffer/hunch 0.40.0 → 1.0.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 +3 -3
- package/dist/store/db.js +43 -5
- package/dist/store/hunchStore.js +5 -8
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
5
5
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](https://nodejs.org)
|
|
8
8
|
[](https://modelcontextprotocol.io)
|
|
9
9
|
|
|
10
10
|
> **A linter checks whether code matches a *pattern*. Hunch checks whether code still matches your *architecture*** —
|
|
@@ -140,7 +140,7 @@ unchanged.
|
|
|
140
140
|
## Getting started
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
|
-
npm install -g @davesheffer/hunch # Node ≥
|
|
143
|
+
npm install -g @davesheffer/hunch # Node ≥ 22.13; puts `hunch` on your PATH
|
|
144
144
|
cd your-repo
|
|
145
145
|
hunch init # scaffold .hunch/, index, install hooks, wire up assistants
|
|
146
146
|
hunch backfill --since 90d # cold start: seed decisions from recent git history
|
|
@@ -320,5 +320,5 @@ memory. → [the docs](https://hunch-pi.vercel.app/docs) for the conceptual mode
|
|
|
320
320
|
|
|
321
321
|
## Develop
|
|
322
322
|
|
|
323
|
-
Hunch is open source — pure TypeScript ESM, Node ≥
|
|
323
|
+
Hunch is open source — pure TypeScript ESM, Node ≥ 22.13, licensed **Apache-2.0**. Contributions
|
|
324
324
|
welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the [repo](https://github.com/davesheffer/hunch).
|
package/dist/store/db.js
CHANGED
|
@@ -1,19 +1,57 @@
|
|
|
1
|
-
/** Thin wrapper around
|
|
2
|
-
import
|
|
1
|
+
/** Thin wrapper around node:sqlite for the derived index. */
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
3
|
import { mkdirSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { SCHEMA_SQL } from "./schema.js";
|
|
6
|
+
/** Load node:sqlite while swallowing ONLY its ExperimentalWarning (Node 22–24 still
|
|
7
|
+
* emits it on module load). Hunch's stderr reaches humans, hooks, and MCP clients on
|
|
8
|
+
* every invocation, so the noise would land everywhere; all other warnings pass through. */
|
|
9
|
+
function loadSqlite() {
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const realEmit = process.emitWarning.bind(process);
|
|
12
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
13
|
+
if (String(warning).includes("SQLite is an experimental feature"))
|
|
14
|
+
return;
|
|
15
|
+
realEmit(warning, ...rest);
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return require("node:sqlite");
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
process.emitWarning = realEmit;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const sqlite = loadSqlite();
|
|
6
25
|
export function openDb(sqlitePath) {
|
|
7
26
|
mkdirSync(dirname(sqlitePath), { recursive: true });
|
|
8
|
-
const db = new
|
|
9
|
-
db.
|
|
27
|
+
const db = new sqlite.DatabaseSync(sqlitePath);
|
|
28
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
10
29
|
db.exec(SCHEMA_SQL);
|
|
11
30
|
return db;
|
|
12
31
|
}
|
|
13
32
|
/** In-memory db (tests / ephemeral queries). */
|
|
14
33
|
export function openMemoryDb() {
|
|
15
|
-
const db = new
|
|
34
|
+
const db = new sqlite.DatabaseSync(":memory:");
|
|
16
35
|
db.exec(SCHEMA_SQL);
|
|
17
36
|
return db;
|
|
18
37
|
}
|
|
38
|
+
/** Run `fn` inside one transaction: BEGIN → fn → COMMIT, ROLLBACK on throw.
|
|
39
|
+
* (node:sqlite has no better-sqlite3-style transaction() helper.) */
|
|
40
|
+
export function withTx(db, fn) {
|
|
41
|
+
db.exec("BEGIN");
|
|
42
|
+
try {
|
|
43
|
+
const out = fn();
|
|
44
|
+
db.exec("COMMIT");
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
try {
|
|
49
|
+
db.exec("ROLLBACK");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* connection already rolled back */
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
19
57
|
//# sourceMappingURL=db.js.map
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolve, join } from "node:path";
|
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { toPosixTarget, hunchPathsForDir } from "../core/paths.js";
|
|
16
16
|
import { ENTITY_KINDS } from "../core/types.js";
|
|
17
|
-
import { openDb } from "./db.js";
|
|
17
|
+
import { openDb, withTx } from "./db.js";
|
|
18
18
|
import { RESET_SQL, embedHash } from "./schema.js";
|
|
19
19
|
import { selectEmbedder } from "./embedder.js";
|
|
20
20
|
import { JsonStore } from "./jsonStore.js";
|
|
@@ -188,7 +188,7 @@ export class HunchStore {
|
|
|
188
188
|
reindex() {
|
|
189
189
|
const db = this.db;
|
|
190
190
|
const counts = {};
|
|
191
|
-
|
|
191
|
+
withTx(db, () => {
|
|
192
192
|
db.exec(RESET_SQL);
|
|
193
193
|
const j = (s) => s; // readability marker for JSON-encoded columns
|
|
194
194
|
// Prepare the FTS insert ONCE (after RESET created the table), not per row.
|
|
@@ -263,7 +263,6 @@ export class HunchStore {
|
|
|
263
263
|
counts.runbooks = runbooks.length;
|
|
264
264
|
void j;
|
|
265
265
|
});
|
|
266
|
-
tx();
|
|
267
266
|
// Reconcile embeddings AFTER the FTS rebuild (model-free): drop vectors whose
|
|
268
267
|
// source doc vanished or whose text changed. Embeddings are NOT in RESET_SQL,
|
|
269
268
|
// so this is what keeps them coherent across the many reindex() call sites.
|
|
@@ -317,14 +316,13 @@ export class HunchStore {
|
|
|
317
316
|
const rows = this.db.prepare(`SELECT ref, doc_hash FROM embeddings`).all();
|
|
318
317
|
const del = this.db.prepare(`DELETE FROM embeddings WHERE ref = ?`);
|
|
319
318
|
let pruned = 0;
|
|
320
|
-
|
|
319
|
+
withTx(this.db, () => {
|
|
321
320
|
for (const r of rows)
|
|
322
321
|
if (live.get(r.ref) !== r.doc_hash) {
|
|
323
322
|
del.run(r.ref);
|
|
324
323
|
pruned++;
|
|
325
324
|
}
|
|
326
325
|
});
|
|
327
|
-
tx();
|
|
328
326
|
return pruned;
|
|
329
327
|
}
|
|
330
328
|
/** Embedding coverage for a model: up-to-date vectors vs total docs (doctor). */
|
|
@@ -357,7 +355,7 @@ export class HunchStore {
|
|
|
357
355
|
for (let i = 0; i < todo.length; i += batchSize) {
|
|
358
356
|
const slice = todo.slice(i, i + batchSize);
|
|
359
357
|
const vecs = await embedder.embed(slice.map((d) => `${d.title}\n${d.body}`));
|
|
360
|
-
|
|
358
|
+
withTx(this.db, () => {
|
|
361
359
|
slice.forEach((d, j) => {
|
|
362
360
|
const v = vecs[j];
|
|
363
361
|
if (v) {
|
|
@@ -366,7 +364,6 @@ export class HunchStore {
|
|
|
366
364
|
}
|
|
367
365
|
});
|
|
368
366
|
});
|
|
369
|
-
tx();
|
|
370
367
|
attempted += slice.length;
|
|
371
368
|
opts.onProgress?.(attempted, todo.length);
|
|
372
369
|
}
|
|
@@ -1167,7 +1164,7 @@ function numEnv(name, dflt) {
|
|
|
1167
1164
|
}
|
|
1168
1165
|
/** Pack a vector's exact bytes for SQLite. Explicit offset+length so a SUBARRAY
|
|
1169
1166
|
* view (byteOffset != 0) writes only its slice, not the whole backing buffer.
|
|
1170
|
-
*
|
|
1167
|
+
* node:sqlite copies on bind, so the returned view never aliases the row. */
|
|
1171
1168
|
function vecToBlob(v) {
|
|
1172
1169
|
return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
|
|
1173
1170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Architectural Conformance for AI-generated code: a git-native graph that deterministically blocks AI changes which break your architecture — the semantic invariants (layering, must-reach, dependency direction) pattern-SAST can't express — grounded in the decisions and bugs behind each rule, across any MCP assistant (Claude Code, Cursor, Copilot, Windsurf, Codex).",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"developer-tools"
|
|
36
36
|
],
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
38
|
+
"node": ">=22.13.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"clean": "node --input-type=commonjs -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
@@ -48,15 +48,13 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
|
-
"better-sqlite3": "12.9.0",
|
|
52
51
|
"commander": "^15.0.0",
|
|
53
52
|
"tree-sitter": "0.21.1",
|
|
54
53
|
"tree-sitter-typescript": "^0.23.2",
|
|
55
54
|
"zod": "^4.4.3"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|
|
58
|
-
"@types/
|
|
59
|
-
"@types/node": "^20.19.0",
|
|
57
|
+
"@types/node": "^22.13.0",
|
|
60
58
|
"tsx": "^4.22.4",
|
|
61
59
|
"typescript": "^5.9.3"
|
|
62
60
|
},
|