@davesheffer/hunch 0.37.0 β†’ 0.38.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 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
  });
@@ -1136,7 +1182,43 @@ program
1136
1182
  console.log("");
1137
1183
  }
1138
1184
  console.log(markdown ? renderMarkdown(report) : renderText(report));
1139
- if (reportFailsStrict(report))
1185
+ // ARCHITECTURAL CONFORMANCE: does the RESULTING code still satisfy every recorded
1186
+ // architectural invariant? This is graph-reachability, not a diff β€” so it catches semantic
1187
+ // violations a pattern-matcher / SAST can't express (a controller that now reaches the DB
1188
+ // directly). It must run over the CHANGED code, so re-parse the working tree first β€” but
1189
+ // ONLY when conformance predicates exist (zero cost on repos that don't use them). The
1190
+ // gate cases (--staged / --base / --commit HEAD) all have the working tree AT the change.
1191
+ // Surfaced always; gates the commit/PR under --strict, with the receipt of the why.
1192
+ const hasConformance = store.recs("decisions").some((d) => (d.conformance?.length ?? 0) > 0);
1193
+ if (hasConformance) {
1194
+ indexRepo(store, root, { churn: false }); // refresh the symbol/dep graph from the working tree
1195
+ store.reindex();
1196
+ }
1197
+ const confViolations = hasConformance ? checkConformance(store).filter((c) => !c.satisfied) : [];
1198
+ if (confViolations.length) {
1199
+ if (markdown) {
1200
+ console.log(`\n### β›” Architectural conformance β€” ${confViolations.length} invariant(s) violated\n`);
1201
+ for (const c of confViolations) {
1202
+ const dec = store.json.get("decisions", c.decision);
1203
+ const why = dec?.context ? ` Β· _why: ${dec.context}_` : "";
1204
+ const bug = dec?.caused_by_bug ? ` Β· prevents recurrence of \`${dec.caused_by_bug}\`` : "";
1205
+ console.log(`- β›” **${c.detail}** β€” \`${c.decision}\` "${c.title}"${why}${bug}`);
1206
+ }
1207
+ }
1208
+ else {
1209
+ console.log(`\nβ›” Architectural conformance β€” ${confViolations.length} invariant(s) the code no longer satisfies (an AI change drifted from the architecture):`);
1210
+ for (const c of confViolations) {
1211
+ const dec = store.json.get("decisions", c.decision);
1212
+ console.log(` ${c.detail} (${c.decision} "${c.title}")`);
1213
+ if (dec?.context)
1214
+ console.log(` ↳ why: ${dec.context}`);
1215
+ if (dec?.caused_by_bug)
1216
+ console.log(` ↳ prevents recurrence of: ${dec.caused_by_bug}`);
1217
+ }
1218
+ console.log(` The semantic invariant a linter can't see β€” run \`hunch conform\` for the full picture.`);
1219
+ }
1220
+ }
1221
+ if (reportFailsStrict(report) || (!!opts.strict && confViolations.length > 0))
1140
1222
  process.exitCode = 1;
1141
1223
  store.close();
1142
1224
  });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.37.0",
3
+ "version": "0.38.1",
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",