@davesheffer/hunch 1.8.0 → 1.8.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.
@@ -1,62 +1,25 @@
1
+ import { languageFor } from "./languages.js";
1
2
  import { loadNativeTreeSitter } from "./nativeTreeSitter.js";
2
- const { Parser, typescript, tsx } = loadNativeTreeSitter();
3
- /** Extremely common builtin/array/object/string/promise method names. Member
4
- * calls to these (e.g. `arr.map(...)`) must NOT create call edges to unrelated
5
- * repo symbols that happen to share the name (DESIGN: keep the graph clean). */
6
- const BUILTIN_METHODS = new Set([
7
- "map", "filter", "forEach", "reduce", "find", "findIndex", "some", "every", "includes",
8
- "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "split", "flat", "flatMap",
9
- "indexOf", "lastIndexOf", "keys", "values", "entries", "sort", "reverse", "fill", "at",
10
- "get", "set", "has", "add", "delete", "clear",
11
- "then", "catch", "finally", "all", "race", "resolve", "reject",
12
- "toString", "valueOf", "toJSON", "hasOwnProperty",
13
- "replace", "replaceAll", "trim", "trimStart", "trimEnd", "padStart", "padEnd", "startsWith", "endsWith",
14
- "toLowerCase", "toUpperCase", "charAt", "charCodeAt", "substring", "substr", "repeat", "match", "matchAll",
15
- "call", "apply", "bind", "test", "exec", "now", "parse", "stringify", "from", "of", "isArray", "assign",
16
- "log", "error", "warn", "info", "debug",
17
- ]);
18
- /** Tree-sitter query capturing every construct we care about in one pass. */
19
- const QUERY_SRC = `
20
- (function_declaration name: (identifier) @fn.name) @fn.def
21
- (generator_function_declaration name: (identifier) @fn.name) @fn.def
22
- (method_definition name: (property_identifier) @method.name) @method.def
23
- (class_declaration name: (type_identifier) @class.name) @class.def
24
- (interface_declaration name: (type_identifier) @iface.name) @iface.def
25
- (type_alias_declaration name: (type_identifier) @type.name) @type.def
26
- (variable_declarator
27
- name: (identifier) @arrow.name
28
- value: [(arrow_function) (function_expression)]) @arrow.def
29
- (import_statement source: (string) @import.src)
30
- (call_expression function: (identifier) @call.id)
31
- (call_expression function: (member_expression property: (property_identifier) @call.member))
32
- `;
3
+ const { Parser } = loadNativeTreeSitter();
33
4
  const cache = new Map();
34
- function bundleFor(lang, key) {
35
- let b = cache.get(key);
5
+ function bundleFor(spec) {
6
+ let b = cache.get(spec.grammarKey);
36
7
  if (!b) {
37
8
  const parser = new Parser();
38
- parser.setLanguage(lang);
39
- const query = new Parser.Query(lang, QUERY_SRC);
9
+ const grammar = spec.loadGrammar();
10
+ parser.setLanguage(grammar);
11
+ const query = new Parser.Query(grammar, spec.query);
40
12
  b = { parser, query };
41
- cache.set(key, b);
13
+ cache.set(spec.grammarKey, b);
42
14
  }
43
15
  return b;
44
16
  }
45
- function pickLanguage(file) {
46
- if (file.endsWith(".tsx") || file.endsWith(".jsx"))
47
- return { lang: tsx, key: "tsx" };
48
- if (file.endsWith(".ts") || file.endsWith(".mts") || file.endsWith(".cts"))
49
- return { lang: typescript, key: "ts" };
50
- if (file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs"))
51
- return { lang: typescript, key: "ts" };
52
- return null;
53
- }
54
17
  const STR_QUOTES = /^['"`]|['"`]$/g;
55
18
  export function parseSource(file, source) {
56
- const picked = pickLanguage(file);
57
- if (!picked)
19
+ const spec = languageFor(file);
20
+ if (!spec)
58
21
  return null;
59
- const { parser, query } = bundleFor(picked.lang, picked.key);
22
+ const { parser, query } = bundleFor(spec);
60
23
  // The native binding caps its scratch buffer at 32 KB unless bufferSize is
61
24
  // given — without this, any source >= 32768 bytes throws "Invalid argument"
62
25
  // and would abort the whole index run. Guard with try/catch as a backstop.
@@ -73,29 +36,27 @@ export function parseSource(file, source) {
73
36
  // group captures by their enclosing @*.def via a quick pass: we record names
74
37
  // keyed by the def node, then emit a symbol per def.
75
38
  const pendingDefs = new Map();
76
- const defKind = {
77
- "fn.def": "function", "method.def": "method", "class.def": "class",
78
- "iface.def": "interface", "type.def": "type", "arrow.def": "function",
79
- };
80
- const nameToDef = {
81
- "fn.name": "fn.def", "method.name": "method.def", "class.name": "class.def",
82
- "iface.name": "iface.def", "type.name": "type.def", "arrow.name": "arrow.def",
83
- };
84
39
  for (const cap of query.captures(tree.rootNode)) {
85
40
  const cname = cap.name;
86
41
  const node = cap.node;
87
42
  if (cname.endsWith(".def")) {
88
- pendingDefs.set(node.id, { kind: defKind[cname], def: node });
43
+ // Keep the FIRST classification a node id receives: a query may have
44
+ // several patterns matching the same node at different specificity
45
+ // (e.g. a Python method inside a class body matches both a class-nested
46
+ // "method.def" pattern and a general "fn.def" pattern — Task 4 relies on
47
+ // this to classify methods correctly without special-casing Python here).
48
+ if (!pendingDefs.has(node.id))
49
+ pendingDefs.set(node.id, { kind: spec.defKindOf[cname], def: node });
89
50
  }
90
- else if (nameToDef[cname]) {
51
+ else if (spec.nameToDef[cname]) {
91
52
  // name capture: find its parent def node id by walking up to the def type
92
- const defNode = ascendToDef(node);
53
+ const defNode = ascendToDef(node, spec.defNodeTypes);
93
54
  if (defNode) {
94
55
  const existing = pendingDefs.get(defNode.id);
95
56
  if (existing)
96
57
  existing.name = node.text;
97
58
  else
98
- pendingDefs.set(defNode.id, { kind: defKind[nameToDef[cname]], def: defNode, name: node.text });
59
+ pendingDefs.set(defNode.id, { kind: spec.defKindOf[spec.nameToDef[cname]], def: defNode, name: node.text });
99
60
  }
100
61
  }
101
62
  else if (cname === "import.src") {
@@ -106,7 +67,7 @@ export function parseSource(file, source) {
106
67
  }
107
68
  else if (cname === "call.member") {
108
69
  // skip builtin method names to avoid false edges to similarly-named symbols
109
- if (!BUILTIN_METHODS.has(node.text))
70
+ if (!spec.builtinMethods.has(node.text))
110
71
  calls.push({ callee: node.text, atByte: node.startIndex, endByte: node.endIndex, member: true });
111
72
  }
112
73
  }
@@ -123,15 +84,11 @@ export function parseSource(file, source) {
123
84
  symbols.sort((a, b) => a.startByte - b.startByte);
124
85
  return { symbols, imports, calls, parseable: !tree.rootNode.hasError };
125
86
  }
126
- /** Walk up to the nearest node whose type is a definition we recognize. */
127
- function ascendToDef(node) {
128
- const defTypes = new Set([
129
- "function_declaration", "generator_function_declaration", "method_definition",
130
- "class_declaration", "interface_declaration", "type_alias_declaration", "variable_declarator",
131
- ]);
87
+ /** Walk up to the nearest node whose type is a definition this language recognizes. */
88
+ function ascendToDef(node, defNodeTypes) {
132
89
  let cur = node.parent;
133
90
  while (cur) {
134
- if (defTypes.has(cur.type))
91
+ if (defNodeTypes.has(cur.type))
135
92
  return cur;
136
93
  cur = cur.parent;
137
94
  }
@@ -35,7 +35,8 @@ export function renderHunchSection(store, root) {
35
35
  lines.push("- `hunch_context(target_or_task)` — the minimal relevant slice for what you're about to do; a task phrase falls back to the closest graph matches. **Call FIRST.**");
36
36
  lines.push("- `hunch_structure(target?)` — the indexed shape of the repo/dir/file/symbol — orient from the graph, not grep rounds.");
37
37
  lines.push("- `hunch_runbook(task)` — the proven steps for a recurring task, before re-deriving them.");
38
- lines.push("- `hunch now` (CLI) — recent decisions + the live roadmap.");
38
+ lines.push("- `hunch_escalations()`the decisions only the HUMAN can make (topic conflicts, candidate/proposed rules, repaired rules needing a re-prove). Normally empty; when it isn't, ASK the user inline — an entry is a question, never an approval.");
39
+ lines.push("- `hunch now` (CLI) — recent decisions + the live roadmap; `hunch log` — the memory-move timeline (every capture/adopt/supersede/prune/repair, each revertable).");
39
40
  lines.push("");
40
41
  lines.push("**Before designing / choosing an approach:**");
41
42
  lines.push("- `hunch_why(target)` — why a file/symbol is shaped this way (decisions, bugs, constraints) — including what was already REJECTED.");
@@ -3,7 +3,7 @@
3
3
  * "add embeddings once keyword search proves insufficient" upgrade).
4
4
  *
5
5
  * Embeddings are LOCAL and FREE. Anthropic has no embeddings endpoint and the
6
- * project is subscription-only (see synthesis/provider.ts), so we run a small
6
+ * project avoids implicit metered inference (see synthesis/provider.ts), so we run a small
7
7
  * sentence-transformer locally via transformers.js. That library is an OPTIONAL
8
8
  * dependency, dynamically imported — if it isn't installed, `selectEmbedder()`
9
9
  * returns null and the whole feature degrades to pure FTS (the lean-install