@davesheffer/hunch 1.14.0 → 1.15.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.
@@ -8,7 +8,8 @@
8
8
  * `indexRepo` persists that exact scan into the JSON source of truth; its caller
9
9
  * then runs HunchStore.reindex() to refresh the SQLite index.
10
10
  */
11
- import { dirname, posix } from "node:path";
11
+ import { readFileSync } from "node:fs";
12
+ import { dirname, join, posix } from "node:path";
12
13
  import { parseSource, attributeCalls } from "./parse.js";
13
14
  import { symbolId, componentId, edgeId, sha1 } from "../core/ids.js";
14
15
  import { externalImportNodeId, externalPackage } from "../core/externalImports.js";
@@ -135,9 +136,15 @@ export function scanRepo(store, root, opts = {}) {
135
136
  // JS/TS resolver and look unimported.
136
137
  const hasSrcLayout = [...fileSymbols.keys()].some((f) => f.startsWith("src/"));
137
138
  const pyRoots = hasSrcLayout ? ["", "src"] : [""];
138
- const resolveImportTarget = (file, spec) => languageFor(file)?.id === "python"
139
- ? resolvePythonImport(file, spec, fileSymbols, pyRoots)
140
- : resolveImport(file, spec, fileSymbols);
139
+ const goModule = readGoModulePath(root);
140
+ const resolveImportTarget = (file, spec) => {
141
+ const langId = languageFor(file)?.id;
142
+ if (langId === "python")
143
+ return resolvePythonImport(file, spec, fileSymbols, pyRoots);
144
+ if (langId === "go")
145
+ return resolveGoImport(spec, fileSymbols, goModule);
146
+ return resolveImport(file, spec, fileSymbols);
147
+ };
141
148
  const importedFiles = new Map(perFileImports.map(({ file, imports }) => [
142
149
  file,
143
150
  new Set(imports.map((specifier) => resolveImportTarget(file, specifier)).filter((target) => !!target)),
@@ -353,6 +360,48 @@ function resolvePythonImport(fromFile, spec, fileSymbols, pyRoots) {
353
360
  const modulePath = baseDir ? `${baseDir}/${tailPath}` : tailPath;
354
361
  return firstExistingPyModule(modulePath, fileSymbols);
355
362
  }
363
+ /** The `module` path declared in the repo's go.mod, or null. A resolution HINT
364
+ * only (it widens depends_on edge coverage); reading it best-effort from the
365
+ * filesystem never gates a scan. */
366
+ function readGoModulePath(root) {
367
+ try {
368
+ const match = /^module\s+(\S+)/m.exec(readFileSync(join(root, "go.mod"), "utf8"));
369
+ return match ? match[1] : null;
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ }
375
+ /** Lexicographically-first tracked .go file whose directory is exactly `dir`
376
+ * ("" = repo root) — a Go import names a PACKAGE (directory), so any file in it
377
+ * identifies the right component for a depends_on edge. */
378
+ function firstGoFileInDir(dir, fileSymbols) {
379
+ let best = null;
380
+ for (const f of fileSymbols.keys()) {
381
+ if (!f.endsWith(".go"))
382
+ continue;
383
+ const d = toPosix(dirname(f));
384
+ const matches = dir === "" ? d === "." : d === dir;
385
+ if (matches && (!best || f < best))
386
+ best = f;
387
+ }
388
+ return best;
389
+ }
390
+ /** Resolve a Go import path to a tracked file. Sibling to resolvePythonImport():
391
+ * an in-module import is the go.mod module path plus the package directory, so
392
+ * strip the declared module prefix and look the directory up exactly; with no
393
+ * go.mod, try the path as a repo-relative directory. Anything else (stdlib,
394
+ * external modules) resolves to null — no suffix guessing, a wrong depends_on
395
+ * edge is worse than a missing one. */
396
+ function resolveGoImport(spec, fileSymbols, goModule) {
397
+ if (goModule) {
398
+ if (spec === goModule)
399
+ return firstGoFileInDir("", fileSymbols);
400
+ if (spec.startsWith(`${goModule}/`))
401
+ return firstGoFileInDir(spec.slice(goModule.length + 1), fileSymbols);
402
+ }
403
+ return firstGoFileInDir(spec, fileSymbols);
404
+ }
356
405
  /** Derive components from the directory layout: the directory immediately under
357
406
  * `src/` (or the top-level dir) groups files into a module component. */
358
407
  function deriveComponents(symbols) {
@@ -125,7 +125,55 @@ const PYTHON = {
125
125
  nameToDef: { "fn.name": "fn.def", "method.name": "method.def", "class.name": "class.def" },
126
126
  builtinMethods: PY_BUILTIN_METHODS,
127
127
  };
128
- export const LANGUAGES = [TYPESCRIPT, TSX, PYTHON];
128
+ const GO_QUERY = `
129
+ (function_declaration name: (identifier) @fn.name) @fn.def
130
+ (method_declaration name: (field_identifier) @method.name) @method.def
131
+ ;; Specific type_spec shapes FIRST: parse.ts keeps the first classification a
132
+ ;; node id receives (same mechanism the Python class patterns rely on), so a
133
+ ;; struct/interface matches its specific pattern before the generic @type.def.
134
+ (type_spec name: (type_identifier) @struct.name type: (struct_type)) @struct.def
135
+ (type_spec name: (type_identifier) @iface.name type: (interface_type)) @iface.def
136
+ (type_spec name: (type_identifier) @type.name) @type.def
137
+ ;; \`type X = Y\` is a distinct type_alias node, not a type_spec.
138
+ (type_alias name: (type_identifier) @type.name) @type.def
139
+ (import_spec path: [(interpreted_string_literal) (raw_string_literal)] @import.src)
140
+ (call_expression function: (identifier) @call.id)
141
+ (call_expression function: (selector_expression field: (field_identifier) @call.member))
142
+ `;
143
+ /** In Go every package-qualified call (fmt.Println, strings.Split, t.Errorf) is a
144
+ * selector_expression and therefore lands in @call.member — the DOMINANT call form.
145
+ * This allowlist filters the highest-frequency stdlib/testing method+function names
146
+ * so they never create false edges to same-named repo symbols; repo-specific member
147
+ * calls (s.Run(), h.Handle()) pass through and resolve conservatively like TS/PY. */
148
+ const GO_BUILTIN_METHODS = new Set([
149
+ "Error", "String", "Read", "Write", "Close", "Len", "Cap", "Reset", "Bytes", "Text",
150
+ "Scan", "Next", "Err", "Lock", "Unlock", "RLock", "RUnlock", "Done", "Add", "Wait",
151
+ "Print", "Printf", "Println", "Sprintf", "Fprintf", "Errorf", "Fatal", "Fatalf", "Fatalln",
152
+ "Log", "Logf", "Helper", "Run", "Parallel", "Skip", "Skipf", "Cleanup",
153
+ "Get", "Set", "Delete", "Load", "Store", "Range", "Value", "Context", "Deadline",
154
+ "Marshal", "Unmarshal", "Encode", "Decode", "Parse", "Format", "Sub", "Before", "After",
155
+ "Join", "Split", "Contains", "Replace", "ReplaceAll", "TrimSpace", "ToLower", "ToUpper",
156
+ "HasPrefix", "HasSuffix", "WriteString", "ReadString", "ReadAll", "Copy", "New", "Now",
157
+ "Since", "Sleep", "Unix", "Exec", "Query", "QueryRow", "Begin", "Commit", "Rollback",
158
+ ]);
159
+ const GO = {
160
+ id: "go",
161
+ extensions: [".go"],
162
+ grammarKey: "go",
163
+ loadGrammar: () => loadNativeTreeSitter().go,
164
+ query: GO_QUERY,
165
+ defNodeTypes: new Set(["function_declaration", "method_declaration", "type_spec", "type_alias"]),
166
+ defKindOf: {
167
+ "fn.def": "function", "method.def": "method", "struct.def": "class",
168
+ "iface.def": "interface", "type.def": "type",
169
+ },
170
+ nameToDef: {
171
+ "fn.name": "fn.def", "method.name": "method.def", "struct.name": "struct.def",
172
+ "iface.name": "iface.def", "type.name": "type.def",
173
+ },
174
+ builtinMethods: GO_BUILTIN_METHODS,
175
+ };
176
+ export const LANGUAGES = [TYPESCRIPT, TSX, PYTHON, GO];
129
177
  export const CODE_EXTENSIONS = [...new Set(LANGUAGES.flatMap((l) => l.extensions))];
130
178
  export function languageFor(file) {
131
179
  for (const lang of LANGUAGES) {
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  const runtimeRequire = createRequire(import.meta.url);
6
6
  const COPY_PREFIX = "hunch-tree-sitter-";
7
- const NATIVE_PACKAGES = ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python"];
7
+ const NATIVE_PACKAGES = ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python", "tree-sitter-go"];
8
8
  let runtime = null;
9
9
  function processIsAlive(pid) {
10
10
  if (pid === process.pid)
@@ -69,7 +69,7 @@ export function loadNativeTreeSitter() {
69
69
  // underscores (tree_sitter_runtime_binding.node, tree_sitter_python_binding.node,
70
70
  // …). Missing the underscore names let an already-loaded source-built addon slip
71
71
  // past this guard and defeat the file-lock isolation entirely (issue #52).
72
- const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
72
+ const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python|-go)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
73
73
  && !new RegExp(`(?:^|[\\\\/])${COPY_PREFIX}\\d+-`).test(path));
74
74
  if (preloaded.length) {
75
75
  throw new Error(`tree-sitter native addon was loaded before Hunch could isolate it: ${preloaded.join(", ")}`);
@@ -87,7 +87,8 @@ export function loadNativeTreeSitter() {
87
87
  const Parser = runtimeRequire("tree-sitter");
88
88
  const languages = runtimeRequire("tree-sitter-typescript");
89
89
  const python = runtimeRequire("tree-sitter-python");
90
- runtime = { Parser, typescript: languages.typescript, tsx: languages.tsx, python };
90
+ const go = runtimeRequire("tree-sitter-go");
91
+ runtime = { Parser, typescript: languages.typescript, tsx: languages.tsx, python, go };
91
92
  }
92
93
  catch (error) {
93
94
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
@@ -74,6 +74,7 @@
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
75
  "commander": "^15.0.0",
76
76
  "tree-sitter": "0.21.1",
77
+ "tree-sitter-go": "^0.23.4",
77
78
  "tree-sitter-python": "^0.23.2",
78
79
  "tree-sitter-typescript": "^0.23.2",
79
80
  "zod": "^4.4.3"
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://hunch-pi.vercel.app",
10
- "version": "1.14.0",
10
+ "version": "1.15.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.14.0",
16
+ "version": "1.15.0",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {