@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.
Files changed (58) hide show
  1. package/README.md +92 -1
  2. package/dist/cli/index.js +1222 -397
  3. package/dist/constitution/adapters.js +31 -14
  4. package/dist/constitution/behaviorEvaluator.js +20 -7
  5. package/dist/constitution/behaviorProof.js +3 -2
  6. package/dist/constitution/canonical.js +7 -1
  7. package/dist/constitution/card.js +7 -2
  8. package/dist/constitution/compiler.js +71 -1
  9. package/dist/constitution/correctionPolicyMaterializer.js +496 -0
  10. package/dist/constitution/delta.js +3 -2
  11. package/dist/constitution/evaluator.js +29 -3
  12. package/dist/constitution/experiment.js +6 -5
  13. package/dist/constitution/experimentRunner.js +43 -14
  14. package/dist/constitution/g2BehaviorCandidates.js +49 -26
  15. package/dist/constitution/g2BehaviorDependencies.js +203 -14
  16. package/dist/constitution/g2Candidates.js +1 -1
  17. package/dist/constitution/lifecycle.js +17 -0
  18. package/dist/constitution/plan.js +26 -9
  19. package/dist/constitution/replacementFreeGit.js +67 -0
  20. package/dist/constitution/replay.js +6 -0
  21. package/dist/constitution/replayCache.js +1 -1
  22. package/dist/constitution/replayWorker.js +1 -1
  23. package/dist/constitution/repository.js +141 -5
  24. package/dist/constitution/safeCheckout.js +75 -0
  25. package/dist/constitution/schema.js +30 -5
  26. package/dist/constitution/service.js +53 -10
  27. package/dist/constitution/sourceMutation.js +65 -12
  28. package/dist/constitution/staticGraphBaseline.js +44 -0
  29. package/dist/constitution/structural.js +60 -4
  30. package/dist/core/autoreview.js +1 -1
  31. package/dist/core/canonicalOrder.js +6 -0
  32. package/dist/core/conformance.js +68 -27
  33. package/dist/core/docscan.js +2 -1
  34. package/dist/core/escalations.js +11 -0
  35. package/dist/core/io.js +44 -9
  36. package/dist/core/overlaySafety.js +178 -0
  37. package/dist/core/paths.js +13 -2
  38. package/dist/core/safeRepoFile.js +74 -0
  39. package/dist/extractors/comments.js +6 -8
  40. package/dist/extractors/git.js +1631 -82
  41. package/dist/extractors/indexer.js +86 -47
  42. package/dist/extractors/repoSource.js +390 -0
  43. package/dist/integrations/ciAction.js +10 -2
  44. package/dist/integrations/gitignore.js +44 -5
  45. package/dist/integrations/mergeDriver.js +23 -5
  46. package/dist/integrations/sync.js +61 -5
  47. package/dist/integrations/team.js +666 -23
  48. package/dist/mcp/server.js +261 -34
  49. package/dist/store/db.js +57 -7
  50. package/dist/store/hunchStore.js +92 -11
  51. package/dist/store/jsonStore.js +350 -63
  52. package/dist/store/schema.js +27 -11
  53. package/dist/synthesis/provider.js +13 -4
  54. package/dist/synthesis/synthesize.js +56 -19
  55. package/dist/wiki/graph.js +5 -4
  56. package/dist/wiki/wiki.js +16 -10
  57. package/package.json +7 -2
  58. package/tooling/md1-benchmark.mjs +628 -0
@@ -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
- this.privateDir = resolve(this.paths.root, priv);
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
- return this.captureHome(isPrivate) === "private" ? this.putPrivate(kind, record) : this.json.put(kind, record);
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
- /** Substring fallback over titles/bodies (handles non-ASCII / malformed FTS). */
315
- likeSearch(query, limit) {
316
- const like = `%${query.replace(/[%_]/g, "")}%`;
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 title LIKE ? OR body LIKE ? LIMIT ?`).all(like, like, limit);
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