@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 +18 -9
- package/dist/cli/index.js +92 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# π§ Hunch β
|
|
1
|
+
# π§ Hunch β Architectural Conformance for AI code
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
4
4
|
[](https://www.npmjs.com/package/@davesheffer/hunch)
|
|
@@ -6,19 +6,28 @@
|
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
[](https://modelcontextprotocol.io)
|
|
8
8
|
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
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 (
|
|
741
|
+
// ---- conform (Architectural Conformance: does the code still satisfy recorded intent) ----
|
|
742
742
|
program
|
|
743
743
|
.command("conform")
|
|
744
|
-
.description("
|
|
745
|
-
.option("--strict", "exit non-zero if any
|
|
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
|
|
752
|
-
console.log(dim(
|
|
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(`
|
|
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}
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
|
-
"description": "
|
|
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",
|