@davesheffer/hunch 0.36.0 → 0.36.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/dist/cli/index.js CHANGED
@@ -946,7 +946,8 @@ program
946
946
  let forbids = deps.length || symbols.length ? { deps, symbols, patterns: [] } : null;
947
947
  let derived = false;
948
948
  if (!forbids && !opts.match) {
949
- forbids = deriveForbids(statement);
949
+ const deps = knownRepoDeps(root);
950
+ forbids = deriveForbids(statement, deps.length ? deps : undefined);
950
951
  derived = !!forbids;
951
952
  }
952
953
  const c = store.json.put("constraints", {
@@ -72,16 +72,32 @@ export function importedDeps(lines) {
72
72
  * correction → enforced constraint) mints a PRECISE matcher, not a scope-only rule that
73
73
  * goes stale. Conservative: only fires on an explicit import/require verb + a
74
74
  * module-shaped token; returns null otherwise (caller falls back to scope-only). */
75
- export function deriveForbids(rule) {
76
- // Only derive a FORBID from a NEGATIVE rule "always use react" must not forbid react.
75
+ export function deriveForbids(rule, knownDeps) {
76
+ // Derive ONLY from a NEGATIVE rule with an UNAMBIGUOUS import verb. "use"/"add" are
77
+ // deliberately excluded: "don't use react hooks" names hooks, not react, and "never use
78
+ // synchronous fs" names no dependency at all — over-deriving there mints a wrong or
79
+ // never-firing rule on the seamless path. Under-deriving is safe (caller falls back to a
80
+ // scope-only rule + the "add --forbid-dep" warning); over-deriving is not.
77
81
  if (!/\b(never|don'?t|do\s+not|avoid|stop|without|ban|banned|forbid|forbidden|prohibit|not\s+allowed|no\s+longer)\b/i.test(rule))
78
82
  return null;
79
- const m = rule.match(/\b(?:import(?:ing)?|requir(?:e|ing)|us(?:e|ing)|add|adopt|depend(?:s|ing)?\s+on)\s+(?:from\s+|the\s+)?["'`]?(@?[a-z0-9][\w.@/-]*)["'`]?/i);
83
+ const m = rule.match(/\b(?:import(?:ing)?|requir(?:e|ing)|depend(?:s|ing)?\s+on)\s+(?:from\s+|the\s+)?["'`]?(@?[a-z0-9][\w.@/-]*)["'`]?/i);
80
84
  if (!m || !m[1])
81
85
  return null;
82
- let dep = m[1].replace(/[).,;:'"`]+$/, "").toLowerCase();
86
+ const dep = m[1].replace(/[).,;:'"`]+$/, "").toLowerCase();
83
87
  if (!dep || dep.length < 2 || /\s/.test(dep) || STOP_TOKENS.has(dep))
84
88
  return null;
89
+ // When the caller can supply the repo's real dependencies, only auto-mint a matcher for a
90
+ // dep that ACTUALLY exists — so a typo or a non-dependency concept ("never import the old
91
+ // helper") doesn't silently create a never-firing rule. Submodule-aware both ways. No list
92
+ // supplied (no/unreadable package.json) → skip validation rather than reject everything.
93
+ if (knownDeps && knownDeps.length) {
94
+ const ok = knownDeps.some((d) => {
95
+ const k = d.toLowerCase();
96
+ return k === dep || dep.startsWith(`${k}/`) || k.startsWith(`${dep}/`);
97
+ });
98
+ if (!ok)
99
+ return null;
100
+ }
85
101
  return { deps: [dep], symbols: [], patterns: [] };
86
102
  }
87
103
  // English words a forbid-verb might grab that are never a dependency.
@@ -71,8 +71,9 @@ export function buildCorrectionConstraint(input, now) {
71
71
  match: null,
72
72
  // Best-effort precise matcher from the rule text ("never import lodash" → forbids lodash),
73
73
  // so the seamless capture path mints enforcement that survives file churn, not a scope-only
74
- // rule that goes stale. null when nothing derivable falls back to scope-based.
75
- forbids: deriveForbids(rule),
74
+ // rule that goes stale. Validated against the repo's real deps when suppliednever mints a
75
+ // never-firing rule for a non-dependency. null when nothing derivable → falls back to scope.
76
+ forbids: deriveForbids(rule, input.knownDeps),
76
77
  rationale: input.rationale ?? "Captured from a human correction of the agent (Never Twice).",
77
78
  source_decision: input.source_decision ?? null,
78
79
  violations: [],
@@ -14,6 +14,7 @@ import { HunchStore } from "../store/hunchStore.js";
14
14
  import { selectEmbedder } from "../store/embedder.js";
15
15
  import { decisionId } from "../core/ids.js";
16
16
  import { buildCorrectionConstraint } from "../core/correction.js";
17
+ import { knownRepoDeps } from "../synthesis/tripwires.js";
17
18
  import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch, pullHunch } from "../extractors/git.js";
18
19
  import { formatContext } from "../core/format.js";
19
20
  import { compareCandidates } from "../core/compare.js";
@@ -360,7 +361,7 @@ export function buildServer(root) {
360
361
  try {
361
362
  if (!input.rule || !input.rule.trim())
362
363
  return err("rule is required — state the invariant in plain words.");
363
- const rec = buildCorrectionConstraint(input, new Date().toISOString());
364
+ const rec = buildCorrectionConstraint({ ...input, knownDeps: knownRepoDeps(root) }, new Date().toISOString());
364
365
  // Private corrections go to the overlay (enforced locally via the merged read,
365
366
  // never rendered into the public CI comment, which is public-only by construction).
366
367
  const existing = input.private ? undefined : store.json.get("constraints", rec.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.36.0",
3
+ "version": "0.36.2",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
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).",