@davesheffer/hunch 0.36.3 β†’ 0.38.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 CHANGED
@@ -1,4 +1,4 @@
1
- # 🧠 Hunch β€” Engineering Memory OS
1
+ # 🧠 Hunch β€” Architectural Conformance for AI code
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@davesheffer/hunch?color=2742ff&label=npm)](https://www.npmjs.com/package/@davesheffer/hunch)
4
4
  [![npm downloads](https://img.shields.io/npm/dw/@davesheffer/hunch?color=2742ff)](https://www.npmjs.com/package/@davesheffer/hunch)
@@ -6,19 +6,28 @@
6
6
  [![node](https://img.shields.io/badge/node-%E2%89%A520-2742ff)](https://nodejs.org)
7
7
  [![MCP](https://img.shields.io/badge/MCP-native-2742ff)](https://modelcontextprotocol.io)
8
8
 
9
- > Git stores *what* the code is. **Hunch** stores ***why*** it is that way β€” a persistent,
10
- > git-native reasoning graph over your codebase, surfaced to Claude Code at reasoning time
11
- > so the AI stops re-deriving understanding and stops undoing intentional design.
12
-
13
- ### ⚑ 60-second start
9
+ > **A linter checks whether code matches a *pattern*. Hunch checks whether code still matches your *architecture*** β€”
10
+ > and blocks the AI change that breaks it, citing the decision and the past bug it would reopen.
11
+ > The semantic invariants pattern-SAST can't express (layering, must-reach, dependency direction),
12
+ > enforced deterministically over a **git-native** graph of *why* β€” across any MCP assistant.
14
13
 
15
14
  ```bash
16
15
  npm i -g @davesheffer/hunch
17
- cd your-repo && hunch init && hunch backfill --since 90d
18
- hunch why src/some/file.ts # …or just ask Claude Code: "why is X built this way?"
16
+ cd your-repo && hunch init
17
+
18
+ # record an architectural invariant β€” the kind Semgrep/SonarQube structurally can't express
19
+ hunch conform --add "controllers never reach the DB directly β€” go through the service layer" \
20
+ --assert not-calls --subject listOrders --object dbQuery --why "the Mar-2025 N+1 meltdown"
21
+
22
+ hunch conform --strict # βœ…/β›” deterministic gate β€” wire into CI; runs on every AI change
19
23
  ```
20
24
 
21
- <sub>Works with **Claude Code, Cursor, Copilot, Windsurf & Google Antigravity** from one shared graph.</sub>
25
+ > An AI "optimizes" the controller to query the DB directly. **Semgrep: green. SonarQube: green.**
26
+ > (it's a legitimate internal import β€” no bad pattern.) **Hunch: β›” BLOCKED** β€” *"listOrders now reaches
27
+ > dbQuery β€” VIOLATED Β· why: the Mar-2025 N+1 meltdown Β· prevents recurrence of bug_0317."* See
28
+ > [`demo/architectural-conformance.sh`](demo/architectural-conformance.sh).
29
+
30
+ <sub>Works with **Claude Code, Cursor, Copilot, Windsurf & Google Antigravity** from one shared, git-native graph.</sub>
22
31
 
23
32
  ### πŸ“š **[Read the full documentation β†’ hunch-pi.vercel.app/docs](https://hunch-pi.vercel.app/docs)**
24
33
 
package/dist/cli/index.js CHANGED
@@ -738,34 +738,80 @@ program
738
738
  console.log(`βœ“ captured ${dec} decision(s) + ${con} constraint(s) from inline comments${opts.private ? " [private overlay]" : ""}`);
739
739
  store.close();
740
740
  });
741
- // ---- conform (intent-conformance: does code still satisfy the recorded why) ----
741
+ // ---- conform (Architectural Conformance: does the code still satisfy recorded intent) ----
742
742
  program
743
743
  .command("conform")
744
- .description("Intent-conformance: prove the code still SATISFIES each in-force decision's recorded intent (deterministic, over the graph). Surfaces where code drifted from the why β€” even with no diff in scope.")
745
- .option("--strict", "exit non-zero if any intent is violated")
744
+ .description("Architectural Conformance: prove the code still SATISFIES each recorded architectural invariant (deterministic, over the graph) β€” the semantic rules pattern-SAST can't express: layering, must-reach, dependency direction. Catches AI changes that pass a linter but break the architecture.")
745
+ .option("--strict", "exit non-zero if any invariant is violated (use as a CI gate)")
746
+ .option("--add <title>", "record an architectural invariant instead of checking, e.g. --add \"controllers never touch the DB directly\"")
747
+ .option("--assert <kind>", "calls | not-calls | imports | not-imports | exists (with --add)")
748
+ .option("--subject <sym>", "the symbol/file:name the invariant is about (with --add)")
749
+ .option("--object <sym>", "the symbol it must reach (calls/imports) or must NOT reach (not-calls/not-imports) (with --add)")
750
+ .option("--transitive", "evaluate reachability transitively, not just direct edges (with --add)")
751
+ .option("--why <text>", "why it holds β€” the rationale, surfaced in the block receipt (with --add)")
752
+ .option("--bug <id>", "the bug id this invariant prevents recurring β€” surfaced in the receipt (with --add)")
746
753
  .action((opts) => {
747
- const { store } = storeFor();
754
+ const { store, root } = storeFor();
755
+ if (opts.add) {
756
+ const ASSERTS = ["calls", "not-calls", "imports", "not-imports", "exists"];
757
+ if (!opts.assert || !ASSERTS.includes(opts.assert))
758
+ return fail(`--assert must be one of: ${ASSERTS.join(", ")}`);
759
+ if (!opts.subject)
760
+ return fail("--subject is required with --add");
761
+ if (opts.assert !== "exists" && !opts.object)
762
+ return fail(`--object is required for --assert ${opts.assert}`);
763
+ store.json.ensureDirs();
764
+ const now = new Date().toISOString();
765
+ const arrow = opts.assert.startsWith("not-") ? "↛" : "β†’";
766
+ const d = store.json.put("decisions", {
767
+ id: decisionId(`conform:${opts.add}:${opts.subject}:${opts.object ?? ""}`),
768
+ title: opts.add,
769
+ status: "accepted",
770
+ context: opts.why ?? "",
771
+ decision: `Architectural invariant: ${opts.subject} ${opts.assert}${opts.object ? ` ${opts.object}` : ""}.`,
772
+ caused_by_bug: opts.bug ?? null,
773
+ conformance: [{ assert: opts.assert, subject: opts.subject, object: opts.assert === "exists" ? undefined : opts.object, transitive: !!opts.transitive }],
774
+ provenance: { source: "human_confirmed", confidence: 1, evidence: [], last_verified: now },
775
+ date: now,
776
+ valid_from: now,
777
+ });
778
+ store.reindex();
779
+ refreshExistingGrounding(root, store); // the invariant reaches every assistant's grounding
780
+ console.log(`βœ“ recorded architectural invariant ${d.id}: "${opts.add}"`);
781
+ console.log(` ${opts.subject} ${arrow} ${opts.object ?? ""}${opts.transitive ? " (transitive)" : ""} [${opts.assert}]`);
782
+ console.log(` enforce on every change: hunch conform --strict (wire into CI alongside hunch ci)`);
783
+ store.close();
784
+ return;
785
+ }
748
786
  store.reindex();
749
787
  const results = checkConformance(store);
750
788
  if (!results.length) {
751
- console.log("No conformance predicates recorded yet.");
752
- console.log(dim(" Add a `conformance` predicate to a decision (e.g. { assert: \"calls\", subject: \"pay\", object: \"verifySession\" }) to prove the code honors its intent."));
789
+ console.log("No architectural invariants recorded yet.");
790
+ console.log(dim(' Record one: hunch conform --add "controllers never touch the DB directly" --assert not-calls --subject OrdersController --object dbQuery'));
753
791
  store.close();
754
792
  return;
755
793
  }
756
794
  const violations = results.filter((r) => !r.satisfied);
757
- console.log(`Intent-conformance: ${results.length - violations.length}/${results.length} satisfied\n`);
795
+ console.log(`Architectural conformance: ${results.length - violations.length}/${results.length} invariants satisfied\n`);
758
796
  for (const r of results) {
759
797
  console.log(` ${r.satisfied ? "βœ…" : "β›”"} ${r.decision} β€” "${r.title}"`);
760
798
  console.log(` ${r.assert} ${r.subject}${r.object ? ` β†’ ${r.object}` : ""}: ${r.detail}`);
799
+ if (!r.satisfied) {
800
+ // The receipt β€” WHY this invariant exists, which pattern-SAST can't tell you.
801
+ const dec = store.json.get("decisions", r.decision);
802
+ if (dec?.context)
803
+ console.log(` ↳ why: ${dec.context}`);
804
+ if (dec?.caused_by_bug)
805
+ console.log(` ↳ prevents recurrence of: ${dec.caused_by_bug}`);
806
+ }
761
807
  }
762
808
  if (violations.length) {
763
- console.log(`\nβ›” ${violations.length} intent(s) the code no longer satisfies.`);
809
+ console.log(`\nβ›” ${violations.length} architectural invariant(s) the code no longer satisfies β€” an AI change drifted from the recorded architecture.`);
764
810
  if (opts.strict)
765
811
  process.exitCode = 1;
766
812
  }
767
813
  else {
768
- console.log(`\nβœ… the code satisfies every recorded intent.`);
814
+ console.log(`\nβœ… the code satisfies every recorded architectural invariant.`);
769
815
  }
770
816
  store.close();
771
817
  });
@@ -968,7 +1014,7 @@ program
968
1014
  provenance: { source: "human_confirmed", confidence: 1, evidence: [], last_verified: new Date().toISOString() },
969
1015
  });
970
1016
  store.reindex();
971
- updateClaudeMd(root, store);
1017
+ refreshExistingGrounding(root, store); // keep EVERY assistant's grounding current, not just CLAUDE.md
972
1018
  console.log(`βœ“ recorded ${c.severity} constraint ${c.id}: "${c.statement}" (scope: ${scope.join(", ") || "repo"})`);
973
1019
  if (derived && c.forbids?.deps.length)
974
1020
  console.log(` ↳ matcher: forbids import of ${c.forbids.deps.join(", ")} (precise, immune to staleness)`);
@@ -1309,6 +1355,45 @@ program
1309
1355
  const next = writeConfig(paths, { firmness: level }).firmness;
1310
1356
  console.log(`βœ“ firmness set to ${next} (takes effect on the next edit β€” no Claude Code restart needed).`);
1311
1357
  });
1358
+ // ---- status (enforcement readiness at a glance) ---------------------------
1359
+ program
1360
+ .command("status")
1361
+ .description("Enforcement readiness at a glance: what's enforcing, what's waiting to confirm, what went stale.")
1362
+ .action(() => {
1363
+ const { store, root } = storeFor();
1364
+ const firmness = readConfig(hunchPaths(root)).firmness;
1365
+ const vouchedSrc = (s) => !!s && (s.includes("human_confirmed") || s === "derived");
1366
+ const blocking = store.recs("constraints").filter((c) => c.status === "active" && c.severity === "blocking" && vouchedSrc(c.provenance?.source));
1367
+ const precise = blocking.filter((c) => !!effectiveForbids(c));
1368
+ const scopeOnly = blocking.filter((c) => !effectiveForbids(c));
1369
+ const drafts = store.json.loadAll("decisions").filter((d) => d.status === "proposed" || d.provenance.confidence < 0.6);
1370
+ const { ready, scrutiny } = partitionReview(drafts, READY_MIN_GROUNDED);
1371
+ const stale = store.staleness((f) => lastChangeDate(f, root)).filter((s) => s.kind === "constraint");
1372
+ const fnote = {
1373
+ off: "not enforcing β€” `hunch firmness advisory` to start",
1374
+ advisory: "surfaces context to the agent; never blocks",
1375
+ firm: "surfaces + warns on a violating edit",
1376
+ strict: "edit-time DENY + CI guard β€” the teeth are on",
1377
+ };
1378
+ console.log(`\nHunch β€” enforcement status (${root.split("/").pop()})\n`);
1379
+ console.log(` firmness: ${firmness} ← ${fnote[firmness] ?? ""}\n`);
1380
+ console.log(` βœ“ ARMED ${blocking.length} confirmed blocking invariant(s) β€” held against every assistant`);
1381
+ if (blocking.length) {
1382
+ console.log(` ${precise.length} precise (block the actual violation, immune to staleness)`);
1383
+ console.log(` ${scopeOnly.length} scope-only (relax to advisory once the file changes)${scopeOnly.length ? " β†’ harden with --forbid-dep" : ""}`);
1384
+ }
1385
+ if (ready.length || scrutiny.length) {
1386
+ console.log(`\n ⏳ TO CONFIRM ${ready.length} ready Β· ${scrutiny.length} need scrutiny β†’ hunch review${ready.length ? " --accept-verified" : ""}`);
1387
+ }
1388
+ if (stale.length) {
1389
+ console.log(`\n β™» STALE ${stale.length} rule(s) whose guarded code moved since last verified β†’ re-confirm to keep the teeth`);
1390
+ }
1391
+ if (firmness !== "strict" && precise.length) {
1392
+ console.log(`\n ⚑ ${precise.length} precise rule(s) are armed but firmness is "${firmness}" β€” set \`hunch firmness strict\` to hard-block them.`);
1393
+ }
1394
+ console.log("");
1395
+ store.close();
1396
+ });
1312
1397
  // ---- hook (Claude Code agent-hook handler) --------------------------------
1313
1398
  program
1314
1399
  .command("hook")
@@ -1446,7 +1531,7 @@ program
1446
1531
  }
1447
1532
  const { source, armed } = acceptDecision(store, d);
1448
1533
  store.reindex();
1449
- updateClaudeMd(root, store);
1534
+ refreshExistingGrounding(root, store); // confirming a rule must reach EVERY assistant's grounding
1450
1535
  console.log(`βœ“ accepted ${opts.accept} (now ${source}, confidence 0.95${armed ? `, ${armed} tripwire(s) now blocking` : ""})`);
1451
1536
  }
1452
1537
  else if (opts.reject) {
@@ -1467,7 +1552,7 @@ program
1467
1552
  for (const it of ready)
1468
1553
  armedTotal += acceptDecision(store, it.d).armed;
1469
1554
  store.reindex();
1470
- updateClaudeMd(root, store);
1555
+ refreshExistingGrounding(root, store); // batch-confirm must reach EVERY assistant's grounding
1471
1556
  console.log(`βœ“ accepted ${ready.length} verified draft(s); ${armedTotal} tripwire(s) now blocking.`);
1472
1557
  for (const it of ready)
1473
1558
  console.log(` ${it.d.id} grounded=${it.synth.grounded ?? "?"} ${it.d.title}`);
@@ -1557,7 +1642,7 @@ program
1557
1642
  if (store.json.delete(c.kind, c.id))
1558
1643
  removed++;
1559
1644
  store.reindex();
1560
- updateClaudeMd(root, store);
1645
+ refreshExistingGrounding(root, store); // removing records must reach EVERY assistant's grounding
1561
1646
  console.log(`\nβœ“ Removed ${removed} record(s).`);
1562
1647
  }
1563
1648
  else {
@@ -15,6 +15,7 @@ import { selectEmbedder } from "../store/embedder.js";
15
15
  import { decisionId } from "../core/ids.js";
16
16
  import { buildCorrectionConstraint } from "../core/correction.js";
17
17
  import { knownRepoDeps } from "../synthesis/tripwires.js";
18
+ import { refreshExistingGrounding } from "../integrations/providers.js";
18
19
  import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch, pullHunch } from "../extractors/git.js";
19
20
  import { formatContext } from "../core/format.js";
20
21
  import { compareCandidates } from "../core/compare.js";
@@ -370,6 +371,12 @@ export function buildServer(root) {
370
371
  else
371
372
  store.json.put("constraints", rec);
372
373
  store.reindex();
374
+ // Propagate the new rule to EVERY assistant's ambient grounding (Cursor/Copilot/
375
+ // Windsurf/AGENTS.md/CLAUDE.md), so a correction captured in one assistant is held
376
+ // by all of them. Public only β€” a private rule must never render into committed
377
+ // grounding. Refresh-only: it never scaffolds a doc the project opted out of.
378
+ if (!input.private)
379
+ refreshExistingGrounding(root, store);
373
380
  let flushed = "";
374
381
  if (input.private && store.privateAutoCommit && store.privateDir) {
375
382
  commitAndPushHunch(store.privateDir, `hunch: capture ${rec.id}`);
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.36.3",
3
+ "version": "0.38.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
- "description": "Hunch β€” an Engineering Memory OS: a persistent, git-native graph of the decisions, bugs, and rules behind your code, served to any MCP coding assistant (Claude Code, Cursor, Copilot, Windsurf, Codex).",
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).",
7
7
  "homepage": "https://hunch-pi.vercel.app",
8
8
  "repository": {
9
9
  "type": "git",