@davesheffer/hunch 1.8.3 → 1.9.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/README.md +92 -1
- package/dist/cli/index.js +1222 -397
- package/dist/constitution/adapters.js +31 -14
- package/dist/constitution/behaviorEvaluator.js +20 -7
- package/dist/constitution/behaviorProof.js +3 -2
- package/dist/constitution/canonical.js +7 -1
- package/dist/constitution/card.js +7 -2
- package/dist/constitution/compiler.js +71 -1
- package/dist/constitution/correctionPolicyMaterializer.js +496 -0
- package/dist/constitution/delta.js +3 -2
- package/dist/constitution/evaluator.js +29 -3
- package/dist/constitution/experiment.js +6 -5
- package/dist/constitution/experimentRunner.js +43 -14
- package/dist/constitution/g2BehaviorCandidates.js +49 -26
- package/dist/constitution/g2BehaviorDependencies.js +203 -14
- package/dist/constitution/g2Candidates.js +1 -1
- package/dist/constitution/lifecycle.js +17 -0
- package/dist/constitution/plan.js +26 -9
- package/dist/constitution/replacementFreeGit.js +67 -0
- package/dist/constitution/replay.js +6 -0
- package/dist/constitution/replayCache.js +1 -1
- package/dist/constitution/replayWorker.js +1 -1
- package/dist/constitution/repository.js +141 -5
- package/dist/constitution/safeCheckout.js +75 -0
- package/dist/constitution/schema.js +30 -5
- package/dist/constitution/service.js +53 -10
- package/dist/constitution/sourceMutation.js +65 -12
- package/dist/constitution/staticGraphBaseline.js +44 -0
- package/dist/constitution/structural.js +60 -4
- package/dist/core/autoreview.js +1 -1
- package/dist/core/canonicalOrder.js +6 -0
- package/dist/core/conformance.js +68 -27
- package/dist/core/docscan.js +2 -1
- package/dist/core/escalations.js +11 -0
- package/dist/core/io.js +44 -9
- package/dist/core/overlaySafety.js +178 -0
- package/dist/core/paths.js +13 -2
- package/dist/core/safeRepoFile.js +74 -0
- package/dist/extractors/comments.js +6 -8
- package/dist/extractors/git.js +1631 -82
- package/dist/extractors/indexer.js +86 -47
- package/dist/extractors/repoSource.js +390 -0
- package/dist/integrations/ciAction.js +10 -2
- package/dist/integrations/gitignore.js +44 -5
- package/dist/integrations/mergeDriver.js +23 -5
- package/dist/integrations/sync.js +61 -5
- package/dist/integrations/team.js +666 -23
- package/dist/mcp/server.js +261 -34
- package/dist/store/db.js +57 -7
- package/dist/store/hunchStore.js +92 -11
- package/dist/store/jsonStore.js +350 -63
- package/dist/store/schema.js +27 -11
- package/dist/synthesis/provider.js +13 -4
- package/dist/synthesis/synthesize.js +56 -19
- package/dist/wiki/graph.js +5 -4
- package/dist/wiki/wiki.js +16 -10
- package/package.json +7 -2
- package/tooling/md1-benchmark.mjs +628 -0
package/dist/store/hunchStore.js
CHANGED
|
@@ -10,21 +10,42 @@
|
|
|
10
10
|
* - bugLineage(): bugs matching a symptom/symbol + their lineage
|
|
11
11
|
* - fragility(): ranked fragility report with evidence
|
|
12
12
|
*/
|
|
13
|
-
import { resolve, join } from "node:path";
|
|
14
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { resolve, join, dirname, isAbsolute, relative } from "node:path";
|
|
14
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
15
15
|
import { toPosixTarget, hunchPathsForDir } from "../core/paths.js";
|
|
16
16
|
import { ENTITY_KINDS } from "../core/types.js";
|
|
17
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";
|
|
21
|
-
import { gitCommonDir } from "../extractors/git.js";
|
|
21
|
+
import { gitCommonDir, gitWorktreeRoot, sameGitPublication } from "../extractors/git.js";
|
|
22
22
|
import { pathMatchesGlob } from "../core/glob.js";
|
|
23
23
|
import { currentForTopic } from "../core/topics.js";
|
|
24
24
|
import { edgeId } from "../core/ids.js";
|
|
25
25
|
import { isStrictBlocker, isVetoBlocker } from "../core/strictgate.js";
|
|
26
26
|
import { effectiveForbids, matchForbids } from "../core/constraintmatch.js";
|
|
27
27
|
import { analyzeDiff } from "../extractors/diff.js";
|
|
28
|
+
/** Git cannot resolve repository identity from a cwd that does not exist yet.
|
|
29
|
+
* Probe the nearest real directory so a planned nested overlay cannot evade the
|
|
30
|
+
* public-repository boundary merely by deferring mkdir until its first write. */
|
|
31
|
+
function nearestExistingDirectory(path) {
|
|
32
|
+
let current = resolve(path);
|
|
33
|
+
while (true) {
|
|
34
|
+
try {
|
|
35
|
+
if (statSync(current).isDirectory())
|
|
36
|
+
return realpathSync(current);
|
|
37
|
+
}
|
|
38
|
+
catch { /* keep walking to an existing ancestor */ }
|
|
39
|
+
const parent = dirname(current);
|
|
40
|
+
if (parent === current)
|
|
41
|
+
return current;
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function pathIsWithin(path, parent) {
|
|
46
|
+
const rel = relative(parent, path);
|
|
47
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel));
|
|
48
|
+
}
|
|
28
49
|
export class HunchStore {
|
|
29
50
|
paths;
|
|
30
51
|
json;
|
|
@@ -65,7 +86,26 @@ export class HunchStore {
|
|
|
65
86
|
const local = this.localConfig();
|
|
66
87
|
const priv = process.env.HUNCH_PRIVATE_DIR?.trim() || local.privateDir;
|
|
67
88
|
if (priv) {
|
|
68
|
-
|
|
89
|
+
const candidate = resolve(this.paths.root, priv);
|
|
90
|
+
const canonical = (path) => { try {
|
|
91
|
+
return realpathSync(path);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return resolve(path);
|
|
95
|
+
} };
|
|
96
|
+
const privateRoot = dirname(canonical(candidate));
|
|
97
|
+
const publicationProbe = nearestExistingDirectory(privateRoot);
|
|
98
|
+
const publicRoot = canonical(this.paths.root);
|
|
99
|
+
const nestedBoundary = pathIsWithin(resolve(candidate), resolve(this.paths.root))
|
|
100
|
+
|| pathIsWithin(publicationProbe, publicRoot);
|
|
101
|
+
const distinctNestedRoot = nestedBoundary ? gitWorktreeRoot(publicationProbe) : null;
|
|
102
|
+
if (canonical(candidate) === canonical(this.paths.hunch) ||
|
|
103
|
+
(nestedBoundary && (!distinctNestedRoot || sameGitPublication(distinctNestedRoot, this.paths.root))) ||
|
|
104
|
+
sameGitPublication(publicationProbe, this.paths.root)) {
|
|
105
|
+
throw new Error(`Unsafe private overlay "${candidate}" shares the public code repository's local or remote publication boundary. ` +
|
|
106
|
+
"Run `hunch private` or `hunch shared --repo <url>` to create a standalone overlay repository.");
|
|
107
|
+
}
|
|
108
|
+
this.privateDir = candidate;
|
|
69
109
|
this.privateJson = new JsonStore(hunchPathsForDir(this.privateDir));
|
|
70
110
|
}
|
|
71
111
|
// Auto-commit is ON unless explicitly opted out (`autoCommit: false` in local.json).
|
|
@@ -92,7 +132,18 @@ export class HunchStore {
|
|
|
92
132
|
* through here so all modes, branches, worktrees, teams, and agents agree on where
|
|
93
133
|
* memory lives. */
|
|
94
134
|
putCapture(kind, record, isPrivate = false) {
|
|
95
|
-
|
|
135
|
+
const home = this.captureHome(isPrivate);
|
|
136
|
+
const id = record.id;
|
|
137
|
+
const targetHasRecord = home === "private" ? !!this.privateJson?.get(kind, id) : !!this.json.get(kind, id);
|
|
138
|
+
const otherHasRecord = home === "private" ? !!this.json.get(kind, id) : !!this.privateJson?.get(kind, id);
|
|
139
|
+
// Legacy repositories can already contain twins, so an idempotent update in
|
|
140
|
+
// the selected home remains possible. A new capture must never CREATE that
|
|
141
|
+
// ambiguous state: merged/private-first reads would make later writers and
|
|
142
|
+
// public renderers disagree about which record is real.
|
|
143
|
+
if (!targetHasRecord && otherHasRecord) {
|
|
144
|
+
throw new Error(`${kind} record ${id} already exists in the other memory home; refusing to create a public/private id collision`);
|
|
145
|
+
}
|
|
146
|
+
return home === "private" ? this.putPrivate(kind, record) : this.json.put(kind, record);
|
|
96
147
|
}
|
|
97
148
|
/** Read a record by id from wherever it lives (private overlay wins on collision). */
|
|
98
149
|
getRec(kind, id) {
|
|
@@ -185,6 +236,10 @@ export class HunchStore {
|
|
|
185
236
|
get hasPrivate() {
|
|
186
237
|
return !!this.privateJson;
|
|
187
238
|
}
|
|
239
|
+
/** Public project root protected from private-overlay commit/push operations. */
|
|
240
|
+
get publicRoot() {
|
|
241
|
+
return this.paths.root;
|
|
242
|
+
}
|
|
188
243
|
/** Write a record into the PRIVATE overlay (never the public repo). Throws if no
|
|
189
244
|
* HUNCH_PRIVATE_DIR is configured, so a "private" write can never silently land
|
|
190
245
|
* in the public `.hunch/`. */
|
|
@@ -204,6 +259,20 @@ export class HunchStore {
|
|
|
204
259
|
this._db?.close();
|
|
205
260
|
this._db = null;
|
|
206
261
|
}
|
|
262
|
+
/** Revision marker for every JSON source currently visible to this store.
|
|
263
|
+
* SQLite/FTS is derived state; long-lived consumers compare this marker with
|
|
264
|
+
* the marker captured only after a successful rebuild. */
|
|
265
|
+
sourceStamp() {
|
|
266
|
+
return `${this.json.changeStamp()}::${this.privateJson?.changeStamp() ?? "no-overlay"}`;
|
|
267
|
+
}
|
|
268
|
+
/** Rebuild from fresh disk reads after an out-of-process Git pull or capture.
|
|
269
|
+
* Normal in-process writes invalidate their own kind cache; cross-process
|
|
270
|
+
* refreshes clear both homes explicitly so schema-only changes are included. */
|
|
271
|
+
reindexFresh() {
|
|
272
|
+
this.json.clearCache();
|
|
273
|
+
this.privateJson?.clearCache();
|
|
274
|
+
return this.reindex();
|
|
275
|
+
}
|
|
207
276
|
// ---- write path ---------------------------------------------------------
|
|
208
277
|
/** Rebuild the entire SQLite index + FTS from the JSON source of truth. */
|
|
209
278
|
reindex() {
|
|
@@ -311,11 +380,23 @@ export class HunchStore {
|
|
|
311
380
|
return this.likeSearch(query, limit);
|
|
312
381
|
}
|
|
313
382
|
}
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
383
|
+
/** Portable bounded fallback over titles/bodies. Each natural-language token
|
|
384
|
+
* is an OR candidate, mirroring the high-recall FTS query closely enough for
|
|
385
|
+
* runtimes whose SQLite build omits the optional FTS5 module. */
|
|
386
|
+
likeSearch(query, limit, kind) {
|
|
387
|
+
const terms = (query.toLowerCase().match(/[\p{L}\p{N}_]+/gu)
|
|
388
|
+
?? [query.toLowerCase().replace(/[%_]/g, "").trim()].filter(Boolean)).slice(0, 32);
|
|
389
|
+
if (!terms.length)
|
|
390
|
+
return [];
|
|
391
|
+
const predicates = terms.map(() => `(lower(title) LIKE ? OR lower(body) LIKE ?)`).join(" OR ");
|
|
392
|
+
const likes = terms.flatMap((term) => {
|
|
393
|
+
const like = `%${term.replace(/[%_]/g, "")}%`;
|
|
394
|
+
return [like, like];
|
|
395
|
+
});
|
|
396
|
+
const where = kind ? `kind = ? AND (${predicates})` : `(${predicates})`;
|
|
397
|
+
const params = kind ? [kind, ...likes, limit] : [...likes, limit];
|
|
317
398
|
const rows = this.db.prepare(`SELECT ref, kind, title, substr(body,1,120) AS snip FROM search
|
|
318
|
-
WHERE
|
|
399
|
+
WHERE ${where} LIMIT ?`).all(...params);
|
|
319
400
|
return rows.map((r) => ({ ref: r.ref, kind: r.kind, title: r.title, snippet: r.snip, score: 0 }));
|
|
320
401
|
}
|
|
321
402
|
// ---- semantic search (opt-in embeddings) --------------------------------
|
|
@@ -557,14 +638,14 @@ export class HunchStore {
|
|
|
557
638
|
scopedFts(query, kind, limit) {
|
|
558
639
|
const match = toFtsQuery(query);
|
|
559
640
|
if (!match)
|
|
560
|
-
return
|
|
641
|
+
return this.likeSearch(query, limit, kind);
|
|
561
642
|
try {
|
|
562
643
|
const rows = this.db.prepare(`SELECT ref, kind, title, snippet(search, 3, '[', ']', '…', 12) AS snip, bm25(search) AS score
|
|
563
644
|
FROM search WHERE search MATCH ? AND kind = ? ORDER BY score LIMIT ?`).all(match, kind, limit);
|
|
564
645
|
return rows.map((r) => ({ ref: r.ref, kind: r.kind, title: r.title, snippet: r.snip, score: r.score }));
|
|
565
646
|
}
|
|
566
647
|
catch {
|
|
567
|
-
return
|
|
648
|
+
return this.likeSearch(query, limit, kind);
|
|
568
649
|
}
|
|
569
650
|
}
|
|
570
651
|
/** Brute-force exact cosine top-n over stored vectors for one model. Vectors are
|